webui: implement modular demo system with TypeScript and shared fixtures

Replace hand-written HTML demo pages with TypeScript demo modules and
automated infrastructure to reduce maintenance overhead and improve
developer experience with type safety and shared code.

Problems Solved:

Demo Maintenance Overhead:
- Hand-written HTML demo pages contained extensive boilerplate duplication
- No type checking for demo setup code or component data
- Manual maintenance of demo/index.html with available demos
- Difficult to share common fake data between demo pages
- No hot module replacement for demo development

Code Quality and Consistency:
- Demo setup code written in plain JavaScript without type safety
- No validation that demo data matches component interfaces
- Inconsistent styling and structure across demo pages
- Duplicated fake data declarations in each demo file

Solution Architecture:

TypeScript Demo Module System:
- Created DemoModule interface for standardized demo structure
- Demo modules export title, description, imports, and setup functions
- Full TypeScript compilation with type checking for demo code
- Dynamic import system for on-demand demo loading with Vite integration

Shared Demo Infrastructure:
- demo-framework/ with types.ts and demo-runner.ts for core functionality
- DemoRunner class handles dynamic loading, cleanup, and error handling
- Single demo-runner.html page loads any demo module dynamically
- Supports URL hash routing for direct demo links

Centralized Fake Data:
- demo-fixtures/ directory with shared TypeScript data files
- sampleToolCalls, sampleTimelineMessages, and sampleContainerState
- Type-safe imports ensure demo data matches component interfaces
- demoUtils with helper functions for consistent demo UI creation

Auto-generated Index Page:
- generate-index.ts scans for *.demo.ts files and extracts metadata
- Creates index-generated.html with links to all available demos
- Automatically includes demo titles and descriptions
- Eliminates manual maintenance of demo listing

Implementation Details:

Demo Framework:
- DemoRunner.loadDemo() uses dynamic imports with Vite ignore comments
- Automatic component import based on demo module configuration
- Support for demo-specific CSS and cleanup functions
- Error handling with detailed error display for debugging

Demo Module Structure:
- sketch-chat-input.demo.ts: Interactive chat with message history
- sketch-container-status.demo.ts: Status variations with real-time updates
- sketch-tool-calls.demo.ts: Multiple tool call examples with progressive loading
- All use shared fixtures and utilities for consistent experience

Vite Integration:
- Hot Module Replacement works for demo modules and shared fixtures
- TypeScript compilation on-the-fly for immediate feedback
- Dynamic imports work seamlessly with Vite's module system
- @vite-ignore comments prevent import analysis warnings

Testing and Validation:
- Tested demo runner loads and displays available components
- Verified component discovery and dynamic import functionality
- Confirmed shared fixture imports work correctly
- Validated auto-generated index creation and content

Files Modified:
- demo-framework/types.ts: TypeScript interfaces for demo system
- demo-framework/demo-runner.ts: Core demo loading and execution logic
- demo-fixtures/: Shared fake data (tool-calls.ts, timeline-messages.ts, container-status.ts, index.ts)
- demo-runner.html: Interactive demo browser with sidebar navigation
- generate-index.ts: Auto-generation script for demo index
- sketch-chat-input.demo.ts: Converted chat input demo to TypeScript
- sketch-container-status.demo.ts: Container status demo with variations
- sketch-tool-calls.demo.ts: Tool calls demo with interactive examples
- readme.md: Comprehensive documentation for new demo system

Benefits:
- Developers get full TypeScript type checking for demo code
- Shared fake data ensures consistency and reduces duplication
- Hot module replacement provides instant feedback during development
- Auto-generated index eliminates manual maintenance
- Modular architecture makes it easy to add new demos
- Vite integration provides fast development iteration

The new system reduces demo maintenance overhead while providing
better developer experience through TypeScript, shared code, and
automated infrastructure.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s3d91894eb7c4a79fk
diff --git a/webui/package-lock.json b/webui/package-lock.json
index 21b15b4..28616e5 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -9,6 +9,8 @@
       "version": "1.0.0",
       "license": "ISC",
       "dependencies": {
+        "@tailwindcss/cli": "^4.1.10",
+        "@tailwindcss/vite": "^4.1.10",
         "@xterm/addon-fit": "^0.10.0",
         "@xterm/xterm": "^5.5.0",
         "dompurify": "^3.2.6",
@@ -17,11 +19,11 @@
         "marked": "^15.0.7",
         "mermaid": "^11.6.0",
         "monaco-editor": "^0.52.2",
-        "sanitize-html": "^2.15.0"
+        "sanitize-html": "^2.15.0",
+        "tailwindcss": "^4.1.10"
       },
       "devDependencies": {
         "@sand4rt/experimental-ct-web": "^1.51.1",
-        "@tailwindcss/cli": "^4.1.10",
         "@types/marked": "^5.0.2",
         "@types/mocha": "^10.0.7",
         "@types/node": "^22.13.14",
@@ -31,7 +33,6 @@
         "esbuild": "^0.25.1",
         "msw": "^2.7.5",
         "prettier": "3.5.3",
-        "tailwindcss": "^4.1.10",
         "typescript": "^5.8.3",
         "vite": "^6.3.4",
         "vite-plugin-web-components-hmr": "^0.1.3"
@@ -41,7 +42,6 @@
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
       "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
-      "dev": true,
       "license": "Apache-2.0",
       "dependencies": {
         "@jridgewell/gen-mapping": "^0.3.5",
@@ -587,7 +587,6 @@
       "cpu": [
         "ppc64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -604,7 +603,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -621,7 +619,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -638,7 +635,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -655,7 +651,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -672,7 +667,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -689,7 +683,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -706,7 +699,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -723,7 +715,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -740,7 +731,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -757,7 +747,6 @@
       "cpu": [
         "ia32"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -774,7 +763,6 @@
       "cpu": [
         "loong64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -791,7 +779,6 @@
       "cpu": [
         "mips64el"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -808,7 +795,6 @@
       "cpu": [
         "ppc64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -825,7 +811,6 @@
       "cpu": [
         "riscv64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -842,7 +827,6 @@
       "cpu": [
         "s390x"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -859,7 +843,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -876,7 +859,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -893,7 +875,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -910,7 +891,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -927,7 +907,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -944,7 +923,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -961,7 +939,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -978,7 +955,6 @@
       "cpu": [
         "ia32"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -995,7 +971,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1123,7 +1098,6 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
       "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "minipass": "^7.0.4"
@@ -1136,7 +1110,6 @@
       "version": "0.3.8",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
       "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@jridgewell/set-array": "^1.2.1",
@@ -1151,7 +1124,6 @@
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
       "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=6.0.0"
@@ -1161,7 +1133,6 @@
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
       "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=6.0.0"
@@ -1171,14 +1142,12 @@
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
       "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/@jridgewell/trace-mapping": {
       "version": "0.3.25",
       "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
       "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@jridgewell/resolve-uri": "^3.1.0",
@@ -1290,7 +1259,6 @@
       "version": "2.5.1",
       "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
       "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
-      "dev": true,
       "hasInstallScript": true,
       "license": "MIT",
       "dependencies": {
@@ -1329,7 +1297,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1350,7 +1317,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1371,7 +1337,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1392,7 +1357,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1413,7 +1377,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1434,7 +1397,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1455,7 +1417,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1476,7 +1437,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1497,7 +1457,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1518,7 +1477,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1539,7 +1497,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1560,7 +1517,6 @@
       "cpu": [
         "ia32"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1581,7 +1537,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1697,7 +1652,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "android"
@@ -1710,7 +1664,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "android"
@@ -1723,7 +1676,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "darwin"
@@ -1736,7 +1688,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "darwin"
@@ -1749,7 +1700,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "freebsd"
@@ -1762,7 +1712,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "freebsd"
@@ -1775,7 +1724,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1788,7 +1736,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1801,7 +1748,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1814,7 +1760,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1827,7 +1772,6 @@
       "cpu": [
         "loong64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1840,7 +1784,6 @@
       "cpu": [
         "ppc64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1853,7 +1796,6 @@
       "cpu": [
         "riscv64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1866,7 +1808,6 @@
       "cpu": [
         "riscv64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1879,7 +1820,6 @@
       "cpu": [
         "s390x"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1892,7 +1832,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1906,7 +1845,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -1919,7 +1857,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "win32"
@@ -1932,7 +1869,6 @@
       "cpu": [
         "ia32"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "win32"
@@ -1945,7 +1881,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "win32"
@@ -1971,7 +1906,6 @@
       "version": "4.1.10",
       "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.10.tgz",
       "integrity": "sha512-TuO7IOUpTG1JeqtMQbQXjR4RIhfZ43mor/vpCp3S5X9h0WxUom5NYgxfNO0PiFoLMJ6/eYCelC7KGvUOmqqK6A==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@parcel/watcher": "^2.5.1",
@@ -1990,7 +1924,6 @@
       "version": "4.1.10",
       "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.10.tgz",
       "integrity": "sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@ampproject/remapping": "^2.3.0",
@@ -2006,7 +1939,6 @@
       "version": "4.1.10",
       "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.10.tgz",
       "integrity": "sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==",
-      "dev": true,
       "hasInstallScript": true,
       "license": "MIT",
       "dependencies": {
@@ -2038,7 +1970,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2055,7 +1986,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2072,7 +2002,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2089,7 +2018,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2106,7 +2034,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2123,7 +2050,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2140,7 +2066,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2157,7 +2082,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2174,7 +2098,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2199,7 +2122,6 @@
       "cpu": [
         "wasm32"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
@@ -2221,7 +2143,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2238,7 +2159,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -2252,12 +2172,25 @@
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
       "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
-      "dev": true,
       "license": "Apache-2.0",
       "engines": {
         "node": ">=8"
       }
     },
+    "node_modules/@tailwindcss/vite": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.10.tgz",
+      "integrity": "sha512-QWnD5HDY2IADv+vYR82lOhqOlS1jSCUUAmfem52cXAhRTKxpDh3ARX8TTXJTCCO7Rv7cD2Nlekabv02bwP3a2A==",
+      "license": "MIT",
+      "dependencies": {
+        "@tailwindcss/node": "4.1.10",
+        "@tailwindcss/oxide": "4.1.10",
+        "tailwindcss": "4.1.10"
+      },
+      "peerDependencies": {
+        "vite": "^5.2.0 || ^6"
+      }
+    },
     "node_modules/@tootallnate/quickjs-emscripten": {
       "version": "0.23.0",
       "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
@@ -2607,7 +2540,6 @@
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
       "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/@types/express": {
@@ -2729,7 +2661,7 @@
       "version": "22.14.0",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz",
       "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==",
-      "dev": true,
+      "devOptional": true,
       "license": "MIT",
       "dependencies": {
         "undici-types": "~6.21.0"
@@ -3577,7 +3509,6 @@
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
       "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "fill-range": "^7.1.1"
@@ -3807,7 +3738,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
       "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
-      "dev": true,
       "license": "BlueOak-1.0.0",
       "engines": {
         "node": ">=18"
@@ -4779,7 +4709,6 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
       "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
-      "dev": true,
       "license": "Apache-2.0",
       "bin": {
         "detect-libc": "bin/detect-libc.js"
@@ -4924,7 +4853,6 @@
       "version": "5.18.1",
       "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
       "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "graceful-fs": "^4.2.4",
@@ -5012,7 +4940,6 @@
       "version": "0.25.2",
       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
       "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
-      "dev": true,
       "hasInstallScript": true,
       "license": "MIT",
       "bin": {
@@ -5270,7 +5197,6 @@
       "version": "7.1.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
       "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "to-regex-range": "^5.0.1"
@@ -5318,7 +5244,6 @@
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
       "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-      "dev": true,
       "hasInstallScript": true,
       "license": "MIT",
       "optional": true,
@@ -5490,7 +5415,6 @@
       "version": "4.2.11",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
       "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
-      "dev": true,
       "license": "ISC"
     },
     "node_modules/graphql": {
@@ -5843,7 +5767,6 @@
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
       "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
@@ -5881,7 +5804,6 @@
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
       "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "is-extglob": "^2.1.1"
@@ -5918,7 +5840,6 @@
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
       "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.12.0"
@@ -6039,7 +5960,6 @@
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
       "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
-      "dev": true,
       "license": "MIT",
       "bin": {
         "jiti": "lib/jiti-cli.mjs"
@@ -6382,7 +6302,6 @@
       "version": "1.30.1",
       "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
       "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
-      "dev": true,
       "license": "MPL-2.0",
       "dependencies": {
         "detect-libc": "^2.0.3"
@@ -6414,7 +6333,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -6435,7 +6353,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -6456,7 +6373,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -6477,7 +6393,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -6498,7 +6413,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -6519,7 +6433,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -6540,7 +6453,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -6561,7 +6473,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -6582,7 +6493,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -6603,7 +6513,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MPL-2.0",
       "optional": true,
       "os": [
@@ -6621,7 +6530,6 @@
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
       "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
-      "dev": true,
       "license": "Apache-2.0",
       "engines": {
         "node": ">=8"
@@ -6726,7 +6634,6 @@
       "version": "0.30.17",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
       "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@jridgewell/sourcemap-codec": "^1.5.0"
@@ -6831,7 +6738,6 @@
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
       "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "braces": "^3.0.3",
@@ -6875,7 +6781,6 @@
       "version": "7.1.2",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
       "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
-      "dev": true,
       "license": "ISC",
       "engines": {
         "node": ">=16 || 14 >=14.17"
@@ -6885,7 +6790,6 @@
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
       "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "minipass": "^7.1.2"
@@ -6904,7 +6808,6 @@
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
       "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
-      "dev": true,
       "license": "MIT",
       "bin": {
         "mkdirp": "dist/cjs/src/bin.js"
@@ -6954,7 +6857,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
       "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=4"
@@ -7076,7 +6978,6 @@
       "version": "7.1.1",
       "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
       "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/node-releases": {
@@ -7389,7 +7290,6 @@
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
       "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8.6"
@@ -8007,7 +7907,6 @@
       "version": "4.39.0",
       "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.39.0.tgz",
       "integrity": "sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==",
-      "dev": true,
       "dependencies": {
         "@types/estree": "1.0.7"
       },
@@ -8487,14 +8386,12 @@
       "version": "4.1.10",
       "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz",
       "integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/tapable": {
       "version": "2.2.2",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
       "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=6"
@@ -8504,7 +8401,6 @@
       "version": "7.4.3",
       "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
       "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
-      "dev": true,
       "license": "ISC",
       "dependencies": {
         "@isaacs/fs-minipass": "^4.0.0",
@@ -8548,7 +8444,6 @@
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
       "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
-      "dev": true,
       "license": "BlueOak-1.0.0",
       "engines": {
         "node": ">=18"
@@ -8579,7 +8474,6 @@
       "version": "0.2.13",
       "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
       "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "fdir": "^6.4.4",
@@ -8596,7 +8490,6 @@
       "version": "6.4.4",
       "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
       "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
-      "dev": true,
       "license": "MIT",
       "peerDependencies": {
         "picomatch": "^3 || ^4"
@@ -8611,7 +8504,6 @@
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
       "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=12"
@@ -8640,7 +8532,6 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
       "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "is-number": "^7.0.0"
@@ -8697,7 +8588,7 @@
       "version": "2.8.1",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
       "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
-      "dev": true,
+      "devOptional": true,
       "license": "0BSD"
     },
     "node_modules/tsscmp": {
@@ -8783,7 +8674,7 @@
       "version": "6.21.0",
       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
       "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
-      "dev": true,
+      "devOptional": true,
       "license": "MIT"
     },
     "node_modules/universalify": {
@@ -8891,7 +8782,6 @@
       "version": "6.3.4",
       "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
       "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "esbuild": "^0.25.0",
@@ -8982,7 +8872,6 @@
       "version": "6.4.4",
       "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
       "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
-      "dev": true,
       "license": "MIT",
       "peerDependencies": {
         "picomatch": "^3 || ^4"
@@ -8997,7 +8886,6 @@
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
       "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=12"
diff --git a/webui/package.json b/webui/package.json
index 8e97fb7..91a1f77 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -16,6 +16,7 @@
     "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",
     "dev": "vite --port 5173 --strictPort --host 127.0.0.1",
     "format": "prettier ./src --write",
     "gentypes": "go run ../cmd/go2ts -o src/types.ts",
@@ -26,8 +27,9 @@
     "test:playwright": "playwright test -c playwright-ct.config.ts"
   },
   "dependencies": {
-    "@xterm/addon-fit": "^0.10.0",
     "@tailwindcss/cli": "^4.1.10",
+    "@tailwindcss/vite": "^4.1.10",
+    "@xterm/addon-fit": "^0.10.0",
     "@xterm/xterm": "^5.5.0",
     "dompurify": "^3.2.6",
     "jsdom": "^26.1.0",
diff --git a/webui/src/web-components/demo/demo-fixtures/container-status.ts b/webui/src/web-components/demo/demo-fixtures/container-status.ts
new file mode 100644
index 0000000..14c4964
--- /dev/null
+++ b/webui/src/web-components/demo/demo-fixtures/container-status.ts
@@ -0,0 +1,99 @@
+/**
+ * Shared fake container status data for demos
+ */
+
+import { State, CumulativeUsage } from "../../../types";
+
+export const sampleUsage: CumulativeUsage = {
+  start_time: "2024-01-15T10:00:00Z",
+  messages: 1337,
+  input_tokens: 25432,
+  output_tokens: 18765,
+  cache_read_input_tokens: 8234,
+  cache_creation_input_tokens: 12354,
+  total_cost_usd: 2.03,
+  tool_uses: {
+    bash: 45,
+    patch: 23,
+    think: 12,
+    "multiple-choice": 8,
+    keyword_search: 6,
+  },
+};
+
+export const sampleContainerState: State = {
+  state_version: 1,
+  message_count: 27,
+  total_usage: sampleUsage,
+  initial_commit: "decafbad42abc123",
+  slug: "file-upload-component",
+  branch_name: "sketch-wip",
+  branch_prefix: "sketch",
+  hostname: "example.hostname",
+  working_dir: "/app",
+  os: "linux",
+  git_origin: "https://github.com/user/repo.git",
+  outstanding_llm_calls: 0,
+  outstanding_tool_calls: null,
+  session_id: "session-abc123",
+  ssh_available: true,
+  in_container: true,
+  first_message_index: 0,
+  agent_state: "ready",
+  outside_hostname: "host.example.com",
+  inside_hostname: "container.local",
+  outside_os: "macOS",
+  inside_os: "linux",
+  outside_working_dir: "/Users/dev/project",
+  inside_working_dir: "/app",
+  todo_content:
+    "- Implement file upload component\n- Add drag and drop support\n- Write tests",
+  skaband_addr: "localhost:8080",
+  link_to_github: true,
+  ssh_connection_string: "ssh user@example.com",
+  diff_lines_added: 245,
+  diff_lines_removed: 67,
+};
+
+export const lightUsageState: State = {
+  ...sampleContainerState,
+  message_count: 5,
+  total_usage: {
+    ...sampleUsage,
+    messages: 5,
+    input_tokens: 1234,
+    output_tokens: 890,
+    total_cost_usd: 0.15,
+    tool_uses: {
+      bash: 2,
+      patch: 1,
+    },
+  },
+  diff_lines_added: 45,
+  diff_lines_removed: 12,
+};
+
+export const heavyUsageState: State = {
+  ...sampleContainerState,
+  message_count: 156,
+  total_usage: {
+    ...sampleUsage,
+    messages: 156,
+    input_tokens: 89234,
+    output_tokens: 67890,
+    cache_read_input_tokens: 23456,
+    cache_creation_input_tokens: 45678,
+    total_cost_usd: 12.45,
+    tool_uses: {
+      bash: 234,
+      patch: 89,
+      think: 67,
+      "multiple-choice": 23,
+      keyword_search: 45,
+      browser_navigate: 12,
+      codereview: 8,
+    },
+  },
+  diff_lines_added: 2847,
+  diff_lines_removed: 1456,
+};
diff --git a/webui/src/web-components/demo/demo-fixtures/index.ts b/webui/src/web-components/demo/demo-fixtures/index.ts
new file mode 100644
index 0000000..9a47f8e
--- /dev/null
+++ b/webui/src/web-components/demo/demo-fixtures/index.ts
@@ -0,0 +1,104 @@
+/**
+ * Centralized exports for all demo fixtures
+ */
+
+// Tool calls
+export {
+  sampleToolCalls,
+  longBashCommand,
+  multipleToolCallGroups,
+} from "./tool-calls";
+
+// Timeline messages
+export {
+  sampleTimelineMessages,
+  longTimelineMessage,
+  mixedTimelineMessages,
+} from "./timeline-messages";
+
+// Container status
+export {
+  sampleUsage,
+  sampleContainerState,
+  lightUsageState,
+  heavyUsageState,
+} from "./container-status";
+
+// Common demo utilities
+export const demoStyles = {
+  container: `
+    max-width: 1200px;
+    margin: 20px auto;
+    padding: 20px;
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+  `,
+
+  demoSection: `
+    margin: 20px 0;
+    padding: 15px;
+    border: 1px solid #e1e5e9;
+    border-radius: 8px;
+    background: #f8f9fa;
+  `,
+
+  demoHeader: `
+    font-size: 18px;
+    font-weight: 600;
+    margin-bottom: 10px;
+    color: #24292f;
+  `,
+};
+
+/**
+ * Common demo setup utilities
+ */
+export const demoUtils = {
+  /**
+   * Create a labeled demo section
+   */
+  createDemoSection(title: string, description?: string): HTMLElement {
+    const section = document.createElement("div");
+    section.style.cssText = demoStyles.demoSection;
+
+    const header = document.createElement("h3");
+    header.style.cssText = demoStyles.demoHeader;
+    header.textContent = title;
+    section.appendChild(header);
+
+    if (description) {
+      const desc = document.createElement("p");
+      desc.textContent = description;
+      desc.style.cssText = "color: #656d76; margin-bottom: 15px;";
+      section.appendChild(desc);
+    }
+
+    return section;
+  },
+
+  /**
+   * Wait for a specified number of milliseconds
+   */
+  delay(ms: number): Promise<void> {
+    return new Promise((resolve) => setTimeout(resolve, ms));
+  },
+
+  /**
+   * Create a simple button for demo interactions
+   */
+  createButton(text: string, onClick: () => void): HTMLButtonElement {
+    const button = document.createElement("button");
+    button.textContent = text;
+    button.style.cssText = `
+      padding: 8px 16px;
+      margin: 5px;
+      background: #0969da;
+      color: white;
+      border: none;
+      border-radius: 6px;
+      cursor: pointer;
+      font-size: 14px;
+    `;
+    button.addEventListener("click", onClick);
+    return button;
+  },
+};
diff --git a/webui/src/web-components/demo/demo-fixtures/timeline-messages.ts b/webui/src/web-components/demo/demo-fixtures/timeline-messages.ts
new file mode 100644
index 0000000..a98c728
--- /dev/null
+++ b/webui/src/web-components/demo/demo-fixtures/timeline-messages.ts
@@ -0,0 +1,113 @@
+/**
+ * Shared fake timeline message data for demos
+ */
+
+import { AgentMessage } from "../../../types";
+import { sampleToolCalls } from "./tool-calls";
+
+const baseTimestamp = new Date("2024-01-15T10:00:00Z");
+
+export const sampleTimelineMessages: AgentMessage[] = [
+  {
+    type: "user",
+    end_of_turn: true,
+    content:
+      "Can you help me implement a file upload component with drag and drop support?",
+    timestamp: new Date(baseTimestamp.getTime()).toISOString(),
+    conversation_id: "demo-conversation",
+    idx: 0,
+  },
+  {
+    type: "agent",
+    end_of_turn: false,
+    content:
+      "I'll help you create a file upload component with drag and drop support. Let me start by analyzing your current project structure and then implement the component.",
+    timestamp: new Date(baseTimestamp.getTime() + 1000).toISOString(),
+    conversation_id: "demo-conversation",
+    idx: 1,
+  },
+  {
+    type: "agent",
+    end_of_turn: false,
+    content: "First, let me check your current directory structure:",
+    tool_calls: [sampleToolCalls[2]], // bash command
+    timestamp: new Date(baseTimestamp.getTime() + 2000).toISOString(),
+    conversation_id: "demo-conversation",
+    idx: 2,
+  },
+  {
+    type: "tool",
+    end_of_turn: false,
+    content:
+      "src/\n├── components/\n│   ├── Button.tsx\n│   └── Input.tsx\n├── styles/\n│   └── globals.css\n└── utils/\n    └── helpers.ts",
+    tool_name: "bash",
+    tool_call_id: "toolu_01bash123",
+    timestamp: new Date(baseTimestamp.getTime() + 3000).toISOString(),
+    conversation_id: "demo-conversation",
+    idx: 3,
+  },
+  {
+    type: "agent",
+    end_of_turn: true,
+    content:
+      "Perfect! I can see you have a components directory. Now I'll create a FileUpload component with drag and drop functionality. This will include:\n\n1. A drop zone area\n2. File selection via click\n3. Progress indicators\n4. File validation\n5. Preview of selected files",
+    timestamp: new Date(baseTimestamp.getTime() + 4000).toISOString(),
+    conversation_id: "demo-conversation",
+    idx: 4,
+  },
+];
+
+export const longTimelineMessage: AgentMessage = {
+  type: "agent",
+  end_of_turn: true,
+  content: `I've analyzed your codebase and here's a comprehensive plan for implementing the file upload component:
+
+## Implementation Plan
+
+### 1. Component Structure
+The FileUpload component will be built using React with TypeScript. It will consist of:
+- A main container with drop zone styling
+- File input element (hidden)
+- Visual feedback for drag states
+- File list display area
+- Progress indicators
+
+### 2. Key Features
+- **Drag & Drop**: Full drag and drop support with visual feedback
+- **Multiple Files**: Support for selecting multiple files at once
+- **File Validation**: Size limits, file type restrictions
+- **Progress Tracking**: Upload progress for each file
+- **Error Handling**: User-friendly error messages
+- **Accessibility**: Proper ARIA labels and keyboard navigation
+
+### 3. Technical Considerations
+- Use the HTML5 File API for file handling
+- Implement proper event handlers for drag events
+- Add debouncing for performance
+- Include comprehensive error boundaries
+- Ensure mobile responsiveness
+
+### 4. Styling Approach
+- CSS modules for component-scoped styles
+- Responsive design with mobile-first approach
+- Smooth animations and transitions
+- Consistent with your existing design system
+
+This implementation will provide a robust, user-friendly file upload experience that integrates seamlessly with your existing application.`,
+  timestamp: new Date(baseTimestamp.getTime() + 5000).toISOString(),
+  conversation_id: "demo-conversation",
+  idx: 5,
+};
+
+export const mixedTimelineMessages: AgentMessage[] = [
+  ...sampleTimelineMessages,
+  longTimelineMessage,
+  {
+    type: "user",
+    end_of_turn: true,
+    content: "That sounds great! Can you also add file type validation?",
+    timestamp: new Date(baseTimestamp.getTime() + 6000).toISOString(),
+    conversation_id: "demo-conversation",
+    idx: 6,
+  },
+];
diff --git a/webui/src/web-components/demo/demo-fixtures/tool-calls.ts b/webui/src/web-components/demo/demo-fixtures/tool-calls.ts
new file mode 100644
index 0000000..eaa5009
--- /dev/null
+++ b/webui/src/web-components/demo/demo-fixtures/tool-calls.ts
@@ -0,0 +1,101 @@
+/**
+ * Shared fake tool call data for demos
+ */
+
+import { ToolCall } from "../../../types";
+
+export const sampleToolCalls: ToolCall[] = [
+  {
+    name: "multiple-choice",
+    input: JSON.stringify({
+      question: "What is your favorite programming language?",
+      choices: [
+        "JavaScript",
+        "TypeScript",
+        "Python",
+        "Go",
+        "Rust",
+        "Java",
+        "C#",
+        "C++",
+      ],
+    }),
+    tool_call_id: "toolu_01choice123",
+    result_message: {
+      type: "tool",
+      end_of_turn: false,
+      content: "Go",
+      tool_result: JSON.stringify({
+        selected: "Go",
+      }),
+      timestamp: new Date().toISOString(),
+      conversation_id: "demo-conversation",
+      idx: 1,
+    },
+  },
+  {
+    name: "multiple-choice",
+    input: JSON.stringify({
+      question: "Which feature would you like to implement next?",
+      choices: [
+        "Dark mode",
+        "User profiles",
+        "Social sharing",
+        "Analytics dashboard",
+      ],
+    }),
+    tool_call_id: "toolu_01choice456",
+    // 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",
+    }),
+    tool_call_id: "toolu_01bash123",
+    result: "Removed containers and pruned images",
+  },
+  {
+    name: "patch",
+    input: JSON.stringify({
+      path: "/app/src/components/Button.tsx",
+      patches: [
+        {
+          operation: "replace",
+          oldText: "className='btn'",
+          newText: "className='btn btn-primary'",
+        },
+      ],
+    }),
+    tool_call_id: "toolu_01patch123",
+    result: "Applied patch successfully",
+  },
+  {
+    name: "think",
+    input: JSON.stringify({
+      thoughts:
+        "I need to analyze the user's requirements and break this down into smaller steps. The user wants to implement a file upload feature with drag-and-drop support.",
+    }),
+    tool_call_id: "toolu_01think123",
+    result: "Recorded thoughts for planning",
+  },
+];
+
+export const longBashCommand: ToolCall = {
+  name: "bash",
+  input: JSON.stringify({
+    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"',
+  }),
+  tool_call_id: "toolu_01longbash",
+  result:
+    "[main abc1234] chore: create empty commit with very long message\n\ncommit created successfully",
+};
+
+export const multipleToolCallGroups = [
+  [sampleToolCalls[0], sampleToolCalls[1]], // Multiple choice examples
+  [sampleToolCalls[2]], // Single bash command
+  [sampleToolCalls[3], sampleToolCalls[4]], // Patch and think
+  [longBashCommand], // Long command example
+];
diff --git a/webui/src/web-components/demo/demo-framework/demo-runner.ts b/webui/src/web-components/demo/demo-framework/demo-runner.ts
new file mode 100644
index 0000000..53f9fba
--- /dev/null
+++ b/webui/src/web-components/demo/demo-framework/demo-runner.ts
@@ -0,0 +1,219 @@
+/**
+ * Demo runner that dynamically loads and executes demo modules
+ */
+
+import {
+  DemoModule,
+  DemoRegistry,
+  DemoRunnerOptions,
+  DemoNavigationEvent,
+} from "./types";
+
+export class DemoRunner {
+  private container: HTMLElement;
+  private basePath: string;
+  private currentDemo: DemoModule | null = null;
+  private currentComponentName: string | null = null;
+  private onDemoChange?: (componentName: string, demo: DemoModule) => void;
+
+  constructor(options: DemoRunnerOptions) {
+    this.container = options.container;
+    this.basePath = options.basePath || "../";
+    this.onDemoChange = options.onDemoChange;
+  }
+
+  /**
+   * Load and display a demo for the specified component
+   */
+  async loadDemo(componentName: string): Promise<void> {
+    try {
+      // Cleanup current demo if any
+      await this.cleanup();
+
+      // Dynamically import the demo module
+      const demoModule = await import(
+        /* @vite-ignore */ `../${componentName}.demo.ts`
+      );
+      const demo: DemoModule = demoModule.default;
+
+      if (!demo) {
+        throw new Error(
+          `Demo module for ${componentName} does not export a default DemoModule`,
+        );
+      }
+
+      // Clear container
+      this.container.innerHTML = "";
+
+      // Load additional styles if specified
+      if (demo.styles) {
+        for (const styleUrl of demo.styles) {
+          await this.loadStylesheet(styleUrl);
+        }
+      }
+
+      // Add custom styles if specified
+      if (demo.customStyles) {
+        this.addCustomStyles(demo.customStyles, componentName);
+      }
+
+      // Import required component modules
+      if (demo.imports) {
+        for (const importPath of demo.imports) {
+          await import(/* @vite-ignore */ this.basePath + importPath);
+        }
+      }
+
+      // Set up the demo
+      await demo.setup(this.container);
+
+      // Update current state
+      this.currentDemo = demo;
+      this.currentComponentName = componentName;
+
+      // Notify listeners
+      if (this.onDemoChange) {
+        this.onDemoChange(componentName, demo);
+      }
+
+      // Dispatch navigation event
+      const event: DemoNavigationEvent = new CustomEvent("demo-navigation", {
+        detail: { componentName, demo },
+      });
+      document.dispatchEvent(event);
+    } catch (error) {
+      console.error(`Failed to load demo for ${componentName}:`, error);
+      this.showError(`Failed to load demo for ${componentName}`, error);
+    }
+  }
+
+  /**
+   * Get list of available demo components by scanning for .demo.ts files
+   */
+  async getAvailableComponents(): Promise<string[]> {
+    // For now, we'll maintain a registry of known demo components
+    // This could be improved with build-time generation
+    const knownComponents = [
+      "sketch-chat-input",
+      "sketch-container-status",
+      "sketch-tool-calls",
+    ];
+
+    // Filter to only components that actually have demo files
+    const availableComponents: string[] = [];
+    for (const component of knownComponents) {
+      try {
+        // Test if the demo module exists by attempting to import it
+        const demoModule = await import(
+          /* @vite-ignore */ `../${component}.demo.ts`
+        );
+        if (demoModule.default) {
+          availableComponents.push(component);
+        }
+      } catch (error) {
+        console.warn(`Demo not available for ${component}:`, error);
+        // Component demo doesn't exist, skip it
+      }
+    }
+
+    return availableComponents;
+  }
+
+  /**
+   * Cleanup current demo
+   */
+  private async cleanup(): Promise<void> {
+    if (this.currentDemo?.cleanup) {
+      await this.currentDemo.cleanup();
+    }
+
+    // Remove custom styles
+    if (this.currentComponentName) {
+      this.removeCustomStyles(this.currentComponentName);
+    }
+
+    this.currentDemo = null;
+    this.currentComponentName = null;
+  }
+
+  /**
+   * Load a CSS stylesheet dynamically
+   */
+  private async loadStylesheet(url: string): Promise<void> {
+    return new Promise((resolve, reject) => {
+      const link = document.createElement("link");
+      link.rel = "stylesheet";
+      link.href = url;
+      link.onload = () => resolve();
+      link.onerror = () =>
+        reject(new Error(`Failed to load stylesheet: ${url}`));
+      document.head.appendChild(link);
+    });
+  }
+
+  /**
+   * Add custom CSS styles for a demo
+   */
+  private addCustomStyles(css: string, componentName: string): void {
+    const styleId = `demo-custom-styles-${componentName}`;
+
+    // Remove existing styles for this component
+    const existing = document.getElementById(styleId);
+    if (existing) {
+      existing.remove();
+    }
+
+    // Add new styles
+    const style = document.createElement("style");
+    style.id = styleId;
+    style.textContent = css;
+    document.head.appendChild(style);
+  }
+
+  /**
+   * Remove custom styles for a component
+   */
+  private removeCustomStyles(componentName: string): void {
+    const styleId = `demo-custom-styles-${componentName}`;
+    const existing = document.getElementById(styleId);
+    if (existing) {
+      existing.remove();
+    }
+  }
+
+  /**
+   * Show error message in the demo container
+   */
+  private showError(message: string, error: any): void {
+    this.container.innerHTML = `
+      <div style="
+        padding: 20px;
+        background: #fee;
+        border: 1px solid #fcc;
+        border-radius: 4px;
+        color: #800;
+        font-family: monospace;
+      ">
+        <h3>Demo Error</h3>
+        <p><strong>${message}</strong></p>
+        <details>
+          <summary>Error Details</summary>
+          <pre>${error.stack || error.message || error}</pre>
+        </details>
+      </div>
+    `;
+  }
+
+  /**
+   * Get current demo info
+   */
+  getCurrentDemo(): { componentName: string; demo: DemoModule } | null {
+    if (this.currentComponentName && this.currentDemo) {
+      return {
+        componentName: this.currentComponentName,
+        demo: this.currentDemo,
+      };
+    }
+    return null;
+  }
+}
diff --git a/webui/src/web-components/demo/demo-framework/types.ts b/webui/src/web-components/demo/demo-framework/types.ts
new file mode 100644
index 0000000..88b1fc5
--- /dev/null
+++ b/webui/src/web-components/demo/demo-framework/types.ts
@@ -0,0 +1,57 @@
+/**
+ * TypeScript interfaces for the demo module system
+ */
+
+export interface DemoModule {
+  /** Display title for the demo */
+  title: string;
+
+  /** Component imports required for this demo */
+  imports: string[];
+
+  /** Additional CSS files to load (optional) */
+  styles?: string[];
+
+  /** Setup function called when demo is loaded */
+  setup: (container: HTMLElement) => void | Promise<void>;
+
+  /** Cleanup function called when demo is unloaded (optional) */
+  cleanup?: () => void | Promise<void>;
+
+  /** Demo-specific CSS styles (optional) */
+  customStyles?: string;
+
+  /** Description of what this demo shows (optional) */
+  description?: string;
+}
+
+/**
+ * Registry of available demo modules
+ */
+export interface DemoRegistry {
+  [componentName: string]: () => Promise<{ default: DemoModule }>;
+}
+
+/**
+ * Options for the demo runner
+ */
+export interface DemoRunnerOptions {
+  /** Container element to render demos in */
+  container: HTMLElement;
+
+  /** Base path for component imports */
+  basePath?: string;
+
+  /** Callback when demo changes */
+  onDemoChange?: (componentName: string, demo: DemoModule) => void;
+}
+
+/**
+ * Event dispatched when demo navigation occurs
+ */
+export interface DemoNavigationEvent extends CustomEvent {
+  detail: {
+    componentName: string;
+    demo: DemoModule;
+  };
+}
diff --git a/webui/src/web-components/demo/demo-runner.html b/webui/src/web-components/demo/demo-runner.html
new file mode 100644
index 0000000..94af314
--- /dev/null
+++ b/webui/src/web-components/demo/demo-runner.html
@@ -0,0 +1,328 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Sketch Web Components Demo Runner</title>
+    <link rel="stylesheet" href="demo.css" />
+    <link rel="stylesheet" href="/dist/tailwind.css" />
+    <style>
+      :root {
+        --demo-primary: #0969da;
+        --demo-secondary: #656d76;
+        --demo-background: #f6f8fa;
+        --demo-border: #d1d9e0;
+      }
+
+      .demo-runner {
+        display: flex;
+        height: 100vh;
+        font-family:
+          -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+      }
+
+      .demo-sidebar {
+        width: 280px;
+        background: var(--demo-background);
+        border-right: 1px solid var(--demo-border);
+        padding: 20px;
+        overflow-y: auto;
+      }
+
+      .demo-content {
+        flex: 1;
+        padding: 20px;
+        overflow-y: auto;
+      }
+
+      .demo-nav {
+        list-style: none;
+        padding: 0;
+        margin: 0;
+      }
+
+      .demo-nav li {
+        margin-bottom: 4px;
+      }
+
+      .demo-nav button {
+        width: 100%;
+        text-align: left;
+        padding: 8px 12px;
+        background: transparent;
+        border: 1px solid transparent;
+        border-radius: 6px;
+        cursor: pointer;
+        font-size: 14px;
+        color: var(--demo-secondary);
+        transition: all 0.2s;
+      }
+
+      .demo-nav button:hover {
+        background: #ffffff;
+        border-color: var(--demo-border);
+        color: var(--demo-primary);
+      }
+
+      .demo-nav button.active {
+        background: var(--demo-primary);
+        color: white;
+      }
+
+      .demo-header {
+        margin-bottom: 20px;
+        padding-bottom: 15px;
+        border-bottom: 1px solid var(--demo-border);
+      }
+
+      .demo-title {
+        font-size: 24px;
+        font-weight: 600;
+        margin: 0 0 8px 0;
+        color: #24292f;
+      }
+
+      .demo-description {
+        color: var(--demo-secondary);
+        margin: 0;
+        font-size: 14px;
+      }
+
+      .demo-container {
+        background: white;
+        border: 1px solid var(--demo-border);
+        border-radius: 8px;
+        min-height: 400px;
+        padding: 20px;
+      }
+
+      .demo-loading {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 200px;
+        color: var(--demo-secondary);
+      }
+
+      .demo-welcome {
+        text-align: center;
+        padding: 60px 20px;
+        color: var(--demo-secondary);
+      }
+
+      .demo-welcome h2 {
+        margin-bottom: 10px;
+        color: #24292f;
+      }
+
+      .search-box {
+        width: 100%;
+        padding: 8px 12px;
+        margin-bottom: 16px;
+        border: 1px solid var(--demo-border);
+        border-radius: 6px;
+        font-size: 14px;
+      }
+
+      .search-box:focus {
+        outline: none;
+        border-color: var(--demo-primary);
+      }
+
+      .demo-error {
+        padding: 20px;
+        background: #ffeaea;
+        border: 1px solid #ffcccc;
+        border-radius: 6px;
+        color: #d73a49;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="demo-runner">
+      <nav class="demo-sidebar">
+        <h1 style="font-size: 18px; margin: 0 0 20px 0; color: #24292f">
+          Component Demos
+        </h1>
+
+        <input
+          type="text"
+          class="search-box"
+          placeholder="Search components..."
+          id="demo-search"
+        />
+
+        <ul class="demo-nav" id="demo-nav">
+          <!-- Component list will be populated dynamically -->
+        </ul>
+      </nav>
+
+      <main class="demo-content">
+        <div class="demo-header" id="demo-header" style="display: none">
+          <h1 class="demo-title" id="demo-title"></h1>
+          <p class="demo-description" id="demo-description"></p>
+        </div>
+
+        <div class="demo-container" id="demo-container">
+          <div class="demo-welcome">
+            <h2>Welcome to Sketch Component Demos</h2>
+            <p>Select a component from the sidebar to view its demo.</p>
+          </div>
+        </div>
+      </main>
+    </div>
+
+    <script type="module">
+      import { DemoRunner } from "./demo-framework/demo-runner.ts";
+
+      class DemoRunnerApp {
+        constructor() {
+          this.demoRunner = new DemoRunner({
+            container: document.getElementById("demo-container"),
+            onDemoChange: this.onDemoChange.bind(this),
+          });
+
+          this.searchBox = document.getElementById("demo-search");
+          this.navList = document.getElementById("demo-nav");
+          this.demoHeader = document.getElementById("demo-header");
+          this.demoTitle = document.getElementById("demo-title");
+          this.demoDescription = document.getElementById("demo-description");
+
+          this.currentComponent = null;
+          this.availableComponents = [];
+
+          this.init();
+        }
+
+        async init() {
+          try {
+            // Load available components
+            this.availableComponents =
+              await this.demoRunner.getAvailableComponents();
+            this.renderNavigation();
+
+            // Set up search
+            this.searchBox.addEventListener(
+              "input",
+              this.handleSearch.bind(this),
+            );
+
+            // Handle URL hash for direct linking
+            this.handleHashChange();
+            window.addEventListener(
+              "hashchange",
+              this.handleHashChange.bind(this),
+            );
+          } catch (error) {
+            console.error("Failed to initialize demo runner:", error);
+            this.showError("Failed to load demo components");
+          }
+        }
+
+        renderNavigation(filter = "") {
+          const filteredComponents = this.availableComponents.filter(
+            (component) =>
+              component.toLowerCase().includes(filter.toLowerCase()),
+          );
+
+          this.navList.innerHTML = "";
+
+          filteredComponents.forEach((component) => {
+            const li = document.createElement("li");
+            const button = document.createElement("button");
+            button.textContent = this.formatComponentName(component);
+            button.addEventListener("click", () =>
+              this.loadComponent(component),
+            );
+
+            if (component === this.currentComponent) {
+              button.classList.add("active");
+            }
+
+            li.appendChild(button);
+            this.navList.appendChild(li);
+          });
+        }
+
+        formatComponentName(component) {
+          return component
+            .replace(/^sketch-/, "")
+            .replace(/-/g, " ")
+            .replace(/\b\w/g, (l) => l.toUpperCase());
+        }
+
+        async loadComponent(componentName) {
+          if (this.currentComponent === componentName) {
+            return;
+          }
+
+          try {
+            this.showLoading();
+            await this.demoRunner.loadDemo(componentName);
+            this.currentComponent = componentName;
+
+            // Update URL hash
+            window.location.hash = componentName;
+
+            // Update navigation
+            this.renderNavigation(this.searchBox.value);
+          } catch (error) {
+            console.error(`Failed to load demo for ${componentName}:`, error);
+            this.showError(`Failed to load demo for ${componentName}`);
+          }
+        }
+
+        onDemoChange(componentName, demo) {
+          // Update header
+          this.demoTitle.textContent = demo.title;
+          this.demoDescription.textContent = demo.description || "";
+
+          if (demo.description) {
+            this.demoDescription.style.display = "block";
+          } else {
+            this.demoDescription.style.display = "none";
+          }
+
+          this.demoHeader.style.display = "block";
+        }
+
+        handleSearch(event) {
+          this.renderNavigation(event.target.value);
+        }
+
+        handleHashChange() {
+          const hash = window.location.hash.slice(1);
+          if (hash && this.availableComponents.includes(hash)) {
+            this.loadComponent(hash);
+          }
+        }
+
+        showLoading() {
+          document.getElementById("demo-container").innerHTML = `
+            <div class="demo-loading">
+              Loading demo...
+            </div>
+          `;
+        }
+
+        showError(message) {
+          document.getElementById("demo-container").innerHTML = `
+            <div class="demo-error">
+              <strong>Error:</strong> ${message}
+            </div>
+          `;
+        }
+      }
+
+      // Initialize the demo runner when DOM is ready
+      if (document.readyState === "loading") {
+        document.addEventListener(
+          "DOMContentLoaded",
+          () => new DemoRunnerApp(),
+        );
+      } else {
+        new DemoRunnerApp();
+      }
+    </script>
+  </body>
+</html>
diff --git a/webui/src/web-components/demo/generate-index.ts b/webui/src/web-components/demo/generate-index.ts
new file mode 100644
index 0000000..015ef88
--- /dev/null
+++ b/webui/src/web-components/demo/generate-index.ts
@@ -0,0 +1,198 @@
+/**
+ * Build-time script to auto-generate demo index page
+ */
+
+import * as fs from "fs";
+import * as path from "path";
+
+interface DemoInfo {
+  name: string;
+  title: string;
+  description?: string;
+  fileName: string;
+}
+
+async function generateIndex() {
+  const demoDir = path.join(__dirname);
+  const files = await fs.promises.readdir(demoDir);
+
+  // Find all .demo.ts files
+  const demoFiles = files.filter((file) => file.endsWith(".demo.ts"));
+
+  const demos: DemoInfo[] = [];
+
+  for (const file of demoFiles) {
+    const componentName = file.replace(".demo.ts", "");
+    const filePath = path.join(demoDir, file);
+
+    try {
+      // Read the file content to extract title and description
+      const content = await fs.promises.readFile(filePath, "utf-8");
+
+      // Extract title from the demo module
+      const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
+      const descriptionMatch = content.match(/description:\s*['"]([^'"]+)['"]/);
+
+      demos.push({
+        name: componentName,
+        title: titleMatch ? titleMatch[1] : formatComponentName(componentName),
+        description: descriptionMatch ? descriptionMatch[1] : undefined,
+        fileName: file,
+      });
+    } catch (error) {
+      console.warn(`Failed to process demo file ${file}:`, error);
+    }
+  }
+
+  // Sort demos alphabetically
+  demos.sort((a, b) => a.title.localeCompare(b.title));
+
+  // Generate HTML index
+  const html = generateIndexHTML(demos);
+
+  // Write the generated index
+  const indexPath = path.join(demoDir, "index-generated.html");
+  await fs.promises.writeFile(indexPath, html, "utf-8");
+
+  console.log(`Generated demo index with ${demos.length} components`);
+  console.log("Available demos:", demos.map((d) => d.name).join(", "));
+}
+
+function formatComponentName(name: string): string {
+  return name
+    .replace(/^sketch-/, "")
+    .replace(/-/g, " ")
+    .replace(/\b\w/g, (l) => l.toUpperCase());
+}
+
+function generateIndexHTML(demos: DemoInfo[]): string {
+  const demoLinks = demos
+    .map((demo) => {
+      const href = `demo-runner.html#${demo.name}`;
+      const description = demo.description ? ` - ${demo.description}` : "";
+
+      return `      <li>
+        <a href="${href}">
+          <strong>${demo.title}</strong>${description}
+        </a>
+      </li>`;
+    })
+    .join("\n");
+
+  return `<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Sketch Web Components - Demo Index</title>
+    <link rel="stylesheet" href="demo.css" />
+    <style>
+      body {
+        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+        max-width: 800px;
+        margin: 40px auto;
+        padding: 20px;
+        line-height: 1.6;
+      }
+      
+      h1 {
+        color: #24292f;
+        border-bottom: 1px solid #d1d9e0;
+        padding-bottom: 10px;
+      }
+      
+      .demo-list {
+        list-style: none;
+        padding: 0;
+      }
+      
+      .demo-list li {
+        margin: 15px 0;
+        padding: 15px;
+        border: 1px solid #d1d9e0;
+        border-radius: 6px;
+        background: #f6f8fa;
+        transition: background-color 0.2s;
+      }
+      
+      .demo-list li:hover {
+        background: #ffffff;
+      }
+      
+      .demo-list a {
+        text-decoration: none;
+        color: #0969da;
+        display: block;
+      }
+      
+      .demo-list a:hover {
+        text-decoration: underline;
+      }
+      
+      .demo-list strong {
+        font-size: 16px;
+        display: block;
+        margin-bottom: 5px;
+      }
+      
+      .stats {
+        background: #fff8dc;
+        padding: 15px;
+        border-radius: 6px;
+        margin: 20px 0;
+        border-left: 4px solid #f9c23c;
+      }
+      
+      .runner-link {
+        display: inline-block;
+        padding: 10px 20px;
+        background: #0969da;
+        color: white;
+        text-decoration: none;
+        border-radius: 6px;
+        margin-top: 20px;
+      }
+      
+      .runner-link:hover {
+        background: #0860ca;
+      }
+    </style>
+  </head>
+  <body>
+    <h1>Sketch Web Components Demo Index</h1>
+    
+    <div class="stats">
+      <strong>Auto-generated index</strong><br>
+      Found ${demos.length} demo component${demos.length === 1 ? "" : "s"} • Last updated: ${new Date().toLocaleString()}
+    </div>
+    
+    <p>
+      This page provides an overview of all available component demos.
+      Click on any component below to view its interactive demo.
+    </p>
+    
+    <a href="demo-runner.html" class="runner-link">🚀 Launch Demo Runner</a>
+    
+    <h2>Available Component Demos</h2>
+    
+    <ul class="demo-list">
+${demoLinks}
+    </ul>
+    
+    <hr style="margin: 40px 0; border: none; border-top: 1px solid #d1d9e0;">
+    
+    <p>
+      <em>This index is automatically generated from available <code>*.demo.ts</code> files.</em><br>
+      To add a new demo, create a <code>component-name.demo.ts</code> file in this directory.
+    </p>
+  </body>
+</html>
+`;
+}
+
+// Run the generator if this script is executed directly
+if (require.main === module) {
+  generateIndex().catch(console.error);
+}
+
+export { generateIndex };
diff --git a/webui/src/web-components/demo/index-generated.html b/webui/src/web-components/demo/index-generated.html
new file mode 100644
index 0000000..7460e78
--- /dev/null
+++ b/webui/src/web-components/demo/index-generated.html
@@ -0,0 +1,130 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Sketch Web Components - Demo Index</title>
+    <link rel="stylesheet" href="demo.css" />
+    <style>
+      body {
+        font-family:
+          -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+        max-width: 800px;
+        margin: 40px auto;
+        padding: 20px;
+        line-height: 1.6;
+      }
+
+      h1 {
+        color: #24292f;
+        border-bottom: 1px solid #d1d9e0;
+        padding-bottom: 10px;
+      }
+
+      .demo-list {
+        list-style: none;
+        padding: 0;
+      }
+
+      .demo-list li {
+        margin: 15px 0;
+        padding: 15px;
+        border: 1px solid #d1d9e0;
+        border-radius: 6px;
+        background: #f6f8fa;
+        transition: background-color 0.2s;
+      }
+
+      .demo-list li:hover {
+        background: #ffffff;
+      }
+
+      .demo-list a {
+        text-decoration: none;
+        color: #0969da;
+        display: block;
+      }
+
+      .demo-list a:hover {
+        text-decoration: underline;
+      }
+
+      .demo-list strong {
+        font-size: 16px;
+        display: block;
+        margin-bottom: 5px;
+      }
+
+      .stats {
+        background: #fff8dc;
+        padding: 15px;
+        border-radius: 6px;
+        margin: 20px 0;
+        border-left: 4px solid #f9c23c;
+      }
+
+      .runner-link {
+        display: inline-block;
+        padding: 10px 20px;
+        background: #0969da;
+        color: white;
+        text-decoration: none;
+        border-radius: 6px;
+        margin-top: 20px;
+      }
+
+      .runner-link:hover {
+        background: #0860ca;
+      }
+    </style>
+  </head>
+  <body>
+    <h1>Sketch Web Components Demo Index</h1>
+
+    <div class="stats">
+      <strong>Auto-generated index</strong><br />
+      Found 3 demo components • Last updated: 6/25/2025, 8:50:21 PM
+    </div>
+
+    <p>
+      This page provides an overview of all available component demos. Click on
+      any component below to view its interactive demo.
+    </p>
+
+    <a href="demo-runner.html" class="runner-link">🚀 Launch Demo Runner</a>
+
+    <h2>Available Component Demos</h2>
+
+    <ul class="demo-list">
+      <li>
+        <a href="demo-runner.html#sketch-chat-input">
+          <strong>Chat Input Demo</strong> - Interactive chat input component
+          with send functionality
+        </a>
+      </li>
+      <li>
+        <a href="demo-runner.html#sketch-container-status">
+          <strong>Container Status Demo</strong> - Display container status
+          information with usage statistics
+        </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>
+    </ul>
+
+    <hr style="margin: 40px 0; border: none; border-top: 1px solid #d1d9e0" />
+
+    <p>
+      <em
+        >This index is automatically generated from available
+        <code>*.demo.ts</code> files.</em
+      ><br />
+      To add a new demo, create a <code>component-name.demo.ts</code> file in
+      this directory.
+    </p>
+  </body>
+</html>
diff --git a/webui/src/web-components/demo/readme.md b/webui/src/web-components/demo/readme.md
index 324d077..686eb63 100644
--- a/webui/src/web-components/demo/readme.md
+++ b/webui/src/web-components/demo/readme.md
@@ -1,5 +1,257 @@
-# Stand-alone demo pages for sketch web components
+# Sketch Web Components Demo System
 
-These are handy for iterating on specific component UI issues in isolation from the rest of the sketch application, and without having to start a full backend to serve the full frontend app UI.
+This directory contains an automated demo system for Sketch web components that reduces maintenance overhead and provides a consistent development experience.
 
-See [README](../../../readme.md#development-mode) for more information on how to run the demo pages.
+## Overview
+
+The demo system consists of:
+
+- **TypeScript Demo Modules** (`*.demo.ts`) - Component-specific demo configurations
+- **Demo Framework** (`demo-framework/`) - Shared infrastructure for loading and running demos
+- **Shared Fixtures** (`demo-fixtures/`) - Common fake data and utilities
+- **Demo Runner** (`demo-runner.html`) - Interactive demo browser
+- **Auto-generated Index** - Automatically maintained list of available demos
+
+## Quick Start
+
+### Running Demos
+
+```bash
+# Start the demo server
+npm run demo
+
+# Visit the demo runner
+open http://localhost:5173/src/web-components/demo/demo-runner.html
+
+# Or view the auto-generated index
+open http://localhost:5173/src/web-components/demo/index-generated.html
+```
+
+### Creating a New Demo
+
+1. Create a new demo module file: `your-component.demo.ts`
+
+```typescript
+import { DemoModule } from "./demo-framework/types";
+import { demoUtils, sampleData } from "./demo-fixtures/index";
+
+const demo: DemoModule = {
+  title: "Your Component Demo",
+  description: "Interactive demo showing component functionality",
+  imports: ["your-component.ts"], // Component files to import
+
+  setup: async (container: HTMLElement) => {
+    // Create demo sections
+    const section = demoUtils.createDemoSection(
+      "Basic Usage",
+      "Description of what this demo shows",
+    );
+
+    // Create your component
+    const component = document.createElement("your-component") as any;
+    component.data = sampleData.yourData;
+
+    // Add to container
+    section.appendChild(component);
+    container.appendChild(section);
+  },
+
+  cleanup: async () => {
+    // Optional cleanup when demo is unloaded
+  },
+};
+
+export default demo;
+```
+
+2. Regenerate the index:
+
+```bash
+cd src/web-components/demo
+npx tsx generate-index.ts
+```
+
+3. Your demo will automatically appear in the demo runner!
+
+## Demo Module Structure
+
+### Required Properties
+
+- `title`: Display name for the demo
+- `imports`: Array of component files to import (relative to parent directory)
+- `setup`: Function that creates the demo content
+
+### Optional Properties
+
+- `description`: Brief description of what the demo shows
+- `styles`: Additional CSS files to load
+- `customStyles`: Inline CSS styles
+- `cleanup`: Function called when demo is unloaded
+
+### Setup Function
+
+The setup function receives a container element and should populate it with demo content:
+
+```typescript
+setup: async (container: HTMLElement) => {
+  // Use demo utilities for consistent styling
+  const section = demoUtils.createDemoSection("Title", "Description");
+
+  // Create and configure your component
+  const component = document.createElement("my-component");
+  component.setAttribute("data", JSON.stringify(sampleData));
+
+  // Add interactive controls
+  const button = demoUtils.createButton("Reset", () => {
+    component.reset();
+  });
+
+  // Assemble the demo
+  section.appendChild(component);
+  section.appendChild(button);
+  container.appendChild(section);
+};
+```
+
+## Shared Fixtures
+
+The `demo-fixtures/` directory contains reusable fake data and utilities:
+
+```typescript
+import {
+  sampleToolCalls,
+  sampleTimelineMessages,
+  sampleContainerState,
+  demoUtils,
+} from "./demo-fixtures/index";
+```
+
+### Available Fixtures
+
+- `sampleToolCalls` - Various tool call examples
+- `sampleTimelineMessages` - Chat/timeline message data
+- `sampleContainerState` - Container status information
+- `demoUtils` - Helper functions for creating demo UI elements
+
+### Demo Utilities
+
+- `demoUtils.createDemoSection(title, description)` - Create a styled demo section
+- `demoUtils.createButton(text, onClick)` - Create a styled button
+- `demoUtils.delay(ms)` - Promise-based delay function
+
+## Benefits of This System
+
+### For Developers
+
+- **TypeScript Support**: Full type checking for demo code and shared data
+- **Hot Module Replacement**: Instant updates when demo code changes
+- **Shared Data**: Consistent fake data across all demos
+- **Reusable Utilities**: Common demo patterns abstracted into utilities
+- **Auto-discovery**: New demos automatically appear in the index
+
+### For Maintenance
+
+- **No Boilerplate**: No need to copy HTML structure between demos
+- **Centralized Styling**: Demo appearance controlled in one place
+- **Automated Index**: Never forget to update the index page
+- **Type Safety**: Catch errors early with TypeScript compilation
+
+## Vite Integration
+
+The system is designed to work seamlessly with Vite:
+
+- **Dynamic Imports**: Demo modules are loaded on demand
+- **TypeScript Compilation**: `.demo.ts` files are compiled automatically
+- **HMR Support**: Changes to demos or fixtures trigger instant reloads
+- **Dependency Tracking**: Vite knows when to reload based on imports
+
+## Migration from HTML Demos
+
+To convert an existing HTML demo:
+
+1. Extract the component setup JavaScript into a `setup` function
+2. Move shared data to `demo-fixtures/`
+3. Replace HTML boilerplate with `demoUtils` calls
+4. Convert inline styles to `customStyles` property
+5. Test with the demo runner
+
+## File Structure
+
+```
+demo/
+├── demo-framework/
+│   ├── types.ts           # TypeScript interfaces
+│   └── demo-runner.ts     # Demo loading and execution
+├── demo-fixtures/
+│   ├── tool-calls.ts      # Tool call sample data
+│   ├── timeline-messages.ts # Message sample data
+│   ├── container-status.ts  # Status sample data
+│   └── index.ts           # Centralized exports
+├── generate-index.ts      # Index generation script
+├── demo-runner.html       # Interactive demo browser
+├── index-generated.html   # Auto-generated index
+├── *.demo.ts             # Individual demo modules
+└── readme.md             # This file
+```
+
+## Advanced Usage
+
+### Custom Styling
+
+```typescript
+const demo: DemoModule = {
+  // ...
+  customStyles: `
+    .my-demo-container {
+      background: #f0f0f0;
+      padding: 20px;
+      border-radius: 8px;
+    }
+  `,
+  setup: async (container) => {
+    container.className = "my-demo-container";
+    // ...
+  },
+};
+```
+
+### Progressive Loading
+
+```typescript
+setup: async (container) => {
+  const messages = [];
+  const timeline = document.createElement("sketch-timeline");
+
+  // Add messages progressively
+  for (let i = 0; i < sampleMessages.length; i++) {
+    await demoUtils.delay(500);
+    messages.push(sampleMessages[i]);
+    timeline.messages = [...messages];
+  }
+};
+```
+
+### Cleanup
+
+```typescript
+let intervalId: number;
+
+const demo: DemoModule = {
+  // ...
+  setup: async (container) => {
+    // Set up interval for updates
+    intervalId = setInterval(() => {
+      updateComponent();
+    }, 1000);
+  },
+
+  cleanup: async () => {
+    // Clean up interval
+    if (intervalId) {
+      clearInterval(intervalId);
+    }
+  },
+};
+```
+
+For more examples, see the existing demo modules in this directory.
diff --git a/webui/src/web-components/demo/sketch-app-shell.demo.html b/webui/src/web-components/demo/sketch-app-shell.demo.html
index 51c3564..651c46a 100644
--- a/webui/src/web-components/demo/sketch-app-shell.demo.html
+++ b/webui/src/web-components/demo/sketch-app-shell.demo.html
@@ -5,7 +5,7 @@
     <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="/src/tailwind.css" />
+    <link rel="stylesheet" href="/dist/tailwind.css" />
 
     <script type="module">
       const { worker } = await import("./mocks/browser");
diff --git a/webui/src/web-components/demo/sketch-chat-input.demo.ts b/webui/src/web-components/demo/sketch-chat-input.demo.ts
new file mode 100644
index 0000000..f18c0b9
--- /dev/null
+++ b/webui/src/web-components/demo/sketch-chat-input.demo.ts
@@ -0,0 +1,114 @@
+/**
+ * Demo module for sketch-chat-input component
+ */
+
+import { DemoModule } from "./demo-framework/types";
+import { demoUtils } from "./demo-fixtures/index";
+
+const demo: DemoModule = {
+  title: "Chat Input Demo",
+  description: "Interactive chat input component with send functionality",
+  imports: ["../sketch-chat-input"],
+
+  setup: async (container: HTMLElement) => {
+    // Create demo sections
+    const basicSection = demoUtils.createDemoSection(
+      "Basic Chat Input",
+      "Type a message and press Enter or click Send",
+    );
+
+    const messagesSection = demoUtils.createDemoSection(
+      "Chat Messages",
+      "Messages will appear here when sent",
+    );
+
+    // Create chat messages container
+    const messagesDiv = document.createElement("div");
+    messagesDiv.id = "chat-messages";
+    messagesDiv.style.cssText = `
+      min-height: 100px;
+      max-height: 200px;
+      overflow-y: auto;
+      border: 1px solid #d1d9e0;
+      border-radius: 6px;
+      padding: 10px;
+      margin-bottom: 10px;
+      background: #f6f8fa;
+    `;
+
+    // Create chat input
+    const chatInput = document.createElement("sketch-chat-input") as any;
+    chatInput.content = "Hello, how can I help you today?";
+
+    // Add message to display
+    const addMessage = (message: string, isUser: boolean = true) => {
+      const messageDiv = document.createElement("div");
+      messageDiv.style.cssText = `
+        padding: 8px 12px;
+        margin: 4px 0;
+        border-radius: 6px;
+        background: ${isUser ? "#0969da" : "#f1f3f4"};
+        color: ${isUser ? "white" : "#24292f"};
+        max-width: 80%;
+        margin-left: ${isUser ? "auto" : "0"};
+        margin-right: ${isUser ? "0" : "auto"};
+      `;
+      messageDiv.textContent = message;
+      messagesDiv.appendChild(messageDiv);
+      messagesDiv.scrollTop = messagesDiv.scrollHeight;
+    };
+
+    // Handle send events
+    chatInput.addEventListener("send-chat", (evt: any) => {
+      const message = evt.detail.message;
+      if (message.trim()) {
+        addMessage(message, true);
+        chatInput.content = "";
+
+        // Simulate bot response after a delay
+        setTimeout(() => {
+          const responses = [
+            "Thanks for your message!",
+            "I understand your request.",
+            "Let me help you with that.",
+            "That's a great question!",
+            "I'll look into that for you.",
+          ];
+          const randomResponse =
+            responses[Math.floor(Math.random() * responses.length)];
+          addMessage(randomResponse, false);
+        }, 1000);
+      }
+    });
+
+    // Add some sample messages
+    addMessage("Welcome to the chat demo!", false);
+    addMessage("This is a sample user message", true);
+
+    // Control buttons
+    const controlsDiv = document.createElement("div");
+    controlsDiv.style.cssText = "margin-top: 15px;";
+
+    const clearButton = demoUtils.createButton("Clear Messages", () => {
+      messagesDiv.innerHTML = "";
+      addMessage("Chat cleared!", false);
+    });
+
+    const presetButton = demoUtils.createButton("Add Preset Message", () => {
+      chatInput.content = "Can you help me implement a file upload component?";
+    });
+
+    controlsDiv.appendChild(clearButton);
+    controlsDiv.appendChild(presetButton);
+
+    // Assemble the demo
+    messagesSection.appendChild(messagesDiv);
+    basicSection.appendChild(chatInput);
+    basicSection.appendChild(controlsDiv);
+
+    container.appendChild(messagesSection);
+    container.appendChild(basicSection);
+  },
+};
+
+export default demo;
diff --git a/webui/src/web-components/demo/sketch-container-status.demo.html b/webui/src/web-components/demo/sketch-container-status.demo.html
index 3b5725b..a48584d 100644
--- a/webui/src/web-components/demo/sketch-container-status.demo.html
+++ b/webui/src/web-components/demo/sketch-container-status.demo.html
@@ -2,7 +2,7 @@
   <head>
     <title>sketch-container-status demo</title>
     <link rel="stylesheet" href="demo.css" />
-    <link rel="stylesheet" href="/src/tailwind.css" />
+    <link rel="stylesheet" href="/dist/tailwind.css" />
     <script type="module" src="../sketch-container-status.ts"></script>
 
     <script>
diff --git a/webui/src/web-components/demo/sketch-container-status.demo.ts b/webui/src/web-components/demo/sketch-container-status.demo.ts
new file mode 100644
index 0000000..20d6a47
--- /dev/null
+++ b/webui/src/web-components/demo/sketch-container-status.demo.ts
@@ -0,0 +1,147 @@
+/**
+ * Demo module for sketch-container-status component
+ */
+
+import { DemoModule } from "./demo-framework/types";
+import {
+  demoUtils,
+  sampleContainerState,
+  lightUsageState,
+  heavyUsageState,
+} from "./demo-fixtures/index";
+
+const demo: DemoModule = {
+  title: "Container Status Demo",
+  description: "Display container status information with usage statistics",
+  imports: ["../sketch-container-status"],
+  styles: ["/dist/tailwind.css"],
+
+  setup: async (container: HTMLElement) => {
+    // Create demo sections
+    const basicSection = demoUtils.createDemoSection(
+      "Basic Container Status",
+      "Shows current container state with usage information",
+    );
+
+    const variationsSection = demoUtils.createDemoSection(
+      "Usage Variations",
+      "Different usage levels and states",
+    );
+
+    // Basic status component
+    const basicStatus = document.createElement(
+      "sketch-container-status",
+    ) as any;
+    basicStatus.id = "basic-status";
+    basicStatus.state = sampleContainerState;
+
+    // Light usage status
+    const lightStatus = document.createElement(
+      "sketch-container-status",
+    ) as any;
+    lightStatus.id = "light-status";
+    lightStatus.state = lightUsageState;
+
+    const lightLabel = document.createElement("h4");
+    lightLabel.textContent = "Light Usage";
+    lightLabel.style.cssText = "margin: 20px 0 10px 0; color: #24292f;";
+
+    // Heavy usage status
+    const heavyStatus = document.createElement(
+      "sketch-container-status",
+    ) as any;
+    heavyStatus.id = "heavy-status";
+    heavyStatus.state = heavyUsageState;
+
+    const heavyLabel = document.createElement("h4");
+    heavyLabel.textContent = "Heavy Usage";
+    heavyLabel.style.cssText = "margin: 20px 0 10px 0; color: #24292f;";
+
+    // Control buttons for interaction
+    const controlsDiv = document.createElement("div");
+    controlsDiv.style.cssText = "margin-top: 20px;";
+
+    const updateBasicButton = demoUtils.createButton(
+      "Update Basic Status",
+      () => {
+        const updatedState = {
+          ...sampleContainerState,
+          message_count: sampleContainerState.message_count + 1,
+          total_usage: {
+            ...sampleContainerState.total_usage!,
+            messages: sampleContainerState.total_usage!.messages + 1,
+            total_cost_usd: Number(
+              (sampleContainerState.total_usage!.total_cost_usd + 0.05).toFixed(
+                2,
+              ),
+            ),
+          },
+        };
+        basicStatus.state = updatedState;
+      },
+    );
+
+    const toggleSSHButton = demoUtils.createButton("Toggle SSH Status", () => {
+      const currentState = basicStatus.state;
+      basicStatus.state = {
+        ...currentState,
+        ssh_available: !currentState.ssh_available,
+        ssh_error: currentState.ssh_available ? "Connection failed" : undefined,
+      };
+    });
+
+    const resetButton = demoUtils.createButton("Reset to Defaults", () => {
+      basicStatus.state = sampleContainerState;
+      lightStatus.state = lightUsageState;
+      heavyStatus.state = heavyUsageState;
+    });
+
+    controlsDiv.appendChild(updateBasicButton);
+    controlsDiv.appendChild(toggleSSHButton);
+    controlsDiv.appendChild(resetButton);
+
+    // Assemble the demo
+    basicSection.appendChild(basicStatus);
+    basicSection.appendChild(controlsDiv);
+
+    variationsSection.appendChild(lightLabel);
+    variationsSection.appendChild(lightStatus);
+    variationsSection.appendChild(heavyLabel);
+    variationsSection.appendChild(heavyStatus);
+
+    container.appendChild(basicSection);
+    container.appendChild(variationsSection);
+
+    // Add some real-time updates
+    const updateInterval = setInterval(() => {
+      const states = [basicStatus, lightStatus, heavyStatus];
+      states.forEach((status) => {
+        if (status.state) {
+          const updatedState = {
+            ...status.state,
+            message_count:
+              status.state.message_count + Math.floor(Math.random() * 2),
+          };
+          if (Math.random() > 0.7) {
+            // 30% chance to update
+            status.state = updatedState;
+          }
+        }
+      });
+    }, 3000);
+
+    // Store interval for cleanup
+    (container as any).demoInterval = updateInterval;
+  },
+
+  cleanup: async () => {
+    // Clear any intervals
+    const container = document.getElementById("demo-container");
+    if (container && (container as any).demoInterval) {
+      clearInterval((container as any).demoInterval);
+      delete (container as any).demoInterval;
+    }
+  },
+};
+
+export default demo;
diff --git a/webui/src/web-components/demo/sketch-tool-calls.demo.ts b/webui/src/web-components/demo/sketch-tool-calls.demo.ts
new file mode 100644
index 0000000..f279657
--- /dev/null
+++ b/webui/src/web-components/demo/sketch-tool-calls.demo.ts
@@ -0,0 +1,138 @@
+/**
+ * Demo module for sketch-tool-calls component
+ */
+
+import { DemoModule } from "./demo-framework/types";
+import {
+  demoUtils,
+  sampleToolCalls,
+  multipleToolCallGroups,
+  longBashCommand,
+} from "./demo-fixtures/index";
+
+const demo: DemoModule = {
+  title: "Tool Calls Demo",
+  description: "Interactive tool call display with various tool types",
+  imports: ["../sketch-tool-calls"],
+
+  setup: async (container: HTMLElement) => {
+    // Create demo sections
+    const basicSection = demoUtils.createDemoSection(
+      "Basic Tool Calls",
+      "Various types of tool calls with results",
+    );
+
+    const interactiveSection = demoUtils.createDemoSection(
+      "Interactive Examples",
+      "Tool calls that can be modified and updated",
+    );
+
+    const groupsSection = demoUtils.createDemoSection(
+      "Tool Call Groups",
+      "Multiple tool calls grouped together",
+    );
+
+    // Basic tool calls component
+    const basicToolCalls = document.createElement("sketch-tool-calls") as any;
+    basicToolCalls.toolCalls = sampleToolCalls.slice(0, 3);
+
+    // Interactive tool calls component
+    const interactiveToolCalls = document.createElement(
+      "sketch-tool-calls",
+    ) as any;
+    interactiveToolCalls.toolCalls = [sampleToolCalls[0]];
+
+    // Control buttons for interaction
+    const controlsDiv = document.createElement("div");
+    controlsDiv.style.cssText = "margin-top: 15px;";
+
+    const addBashButton = demoUtils.createButton("Add Bash Command", () => {
+      const currentCalls = interactiveToolCalls.toolCalls || [];
+      interactiveToolCalls.toolCalls = [...currentCalls, sampleToolCalls[2]];
+    });
+
+    const addLongCommandButton = demoUtils.createButton(
+      "Add Long Command",
+      () => {
+        const currentCalls = interactiveToolCalls.toolCalls || [];
+        interactiveToolCalls.toolCalls = [...currentCalls, longBashCommand];
+      },
+    );
+
+    const clearButton = demoUtils.createButton("Clear Tool Calls", () => {
+      interactiveToolCalls.toolCalls = [];
+    });
+
+    const resetButton = demoUtils.createButton("Reset to Default", () => {
+      interactiveToolCalls.toolCalls = [sampleToolCalls[0]];
+    });
+
+    controlsDiv.appendChild(addBashButton);
+    controlsDiv.appendChild(addLongCommandButton);
+    controlsDiv.appendChild(clearButton);
+    controlsDiv.appendChild(resetButton);
+
+    // Tool call groups
+    const groupsContainer = document.createElement("div");
+    multipleToolCallGroups.forEach((group, index) => {
+      const groupHeader = document.createElement("h4");
+      groupHeader.textContent = `Group ${index + 1}`;
+      groupHeader.style.cssText = "margin: 20px 0 10px 0; color: #24292f;";
+
+      const groupToolCalls = document.createElement("sketch-tool-calls") as any;
+      groupToolCalls.toolCalls = group;
+
+      groupsContainer.appendChild(groupHeader);
+      groupsContainer.appendChild(groupToolCalls);
+    });
+
+    // Progressive loading demo
+    const progressiveSection = demoUtils.createDemoSection(
+      "Progressive Loading Demo",
+      "Tool calls that appear one by one",
+    );
+
+    const progressiveToolCalls = document.createElement(
+      "sketch-tool-calls",
+    ) as any;
+    progressiveToolCalls.toolCalls = [];
+
+    const startProgressiveButton = demoUtils.createButton(
+      "Start Progressive Load",
+      async () => {
+        progressiveToolCalls.toolCalls = [];
+
+        for (let i = 0; i < sampleToolCalls.length; i++) {
+          await demoUtils.delay(1000);
+          const currentCalls = progressiveToolCalls.toolCalls || [];
+          progressiveToolCalls.toolCalls = [
+            ...currentCalls,
+            sampleToolCalls[i],
+          ];
+        }
+      },
+    );
+
+    const progressiveControls = document.createElement("div");
+    progressiveControls.style.cssText = "margin-top: 15px;";
+    progressiveControls.appendChild(startProgressiveButton);
+
+    // Assemble the demo
+    basicSection.appendChild(basicToolCalls);
+
+    interactiveSection.appendChild(interactiveToolCalls);
+    interactiveSection.appendChild(controlsDiv);
+
+    groupsSection.appendChild(groupsContainer);
+
+    progressiveSection.appendChild(progressiveToolCalls);
+    progressiveSection.appendChild(progressiveControls);
+
+    container.appendChild(basicSection);
+    container.appendChild(interactiveSection);
+    container.appendChild(groupsSection);
+    container.appendChild(progressiveSection);
+  },
+};
+
+export default demo;
diff --git a/webui/vite.config.mts b/webui/vite.config.mts
index 7345514..9fc8e3a 100644
--- a/webui/vite.config.mts
+++ b/webui/vite.config.mts
@@ -1,7 +1,11 @@
 import { dirname, resolve } from "node:path";
 import { fileURLToPath } from "node:url";
+import { spawn } from "node:child_process";
 import { hmrPlugin, presets } from "vite-plugin-web-components-hmr";
 import { defineConfig } from "vite";
+import type { Plugin } from "vite";
+import * as fs from "node:fs";
+import tailwindcss from "@tailwindcss/vite";
 
 const __dirname = dirname(fileURLToPath(import.meta.url));
 
@@ -12,6 +16,7 @@
     __MERMAID_HASH__: JSON.stringify("dev"), // Use 'dev' as hash in development
   },
   plugins: [
+    tailwindcss(),
     hmrPlugin({
       include: ["./src/**/*.ts"],
       presets: [presets.lit],