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],