webui: Improve dx
For local development, switch to Vite and update web components for improved demo experience. Note that we haven't changed how we bundle when we're actually running in sketch; that's still the go/esbuild in-memory setup. This just changes demo dev setup to get breakpoints working and a functioning full sketch-app-shell.
We still need to add some mock data, but this is a start
- Introduced `vite.config.mts` for Vite setup with hot module reloading.
- Updated `package.json` and `package-lock.json` to include Vite and related plugins.
- Refactored demo scripts to utilize Vite for local development.
- Created `launch.json` for VSCode debugging configuration.
- Enhanced `Makefile` with a new demo task.
- Improved styling and structure in demo HTML and CSS files.
- Implemented `aggregateAgentMessages` function for message handling in web components.
diff --git a/loop/webui/Makefile b/loop/webui/Makefile
index 662cfe7..2ce27f5 100644
--- a/loop/webui/Makefile
+++ b/loop/webui/Makefile
@@ -3,6 +3,9 @@
install:
npm ci
+demo:
+ npm run demo
+
# TypeScript type checking
# Note: The actual esbuild bundling happens in esbuild.go
check:
diff --git a/loop/webui/package-lock.json b/loop/webui/package-lock.json
index e4b2748..8abb565 100644
--- a/loop/webui/package-lock.json
+++ b/loop/webui/package-lock.json
@@ -20,18 +20,18 @@
"vega-lite": "^5.23.0"
},
"devDependencies": {
- "@open-wc/dev-server-hmr": "^0.1.2-next.0",
"@sand4rt/experimental-ct-web": "^1.51.1",
"@types/marked": "^5.0.2",
"@types/mocha": "^10.0.7",
"@types/node": "^22.13.14",
- "@web/dev-server": "^0.4.6",
"@web/test-runner": "^0.18.2",
"@web/test-runner-puppeteer": "^0.18.0",
"autoprefixer": "^10.4.21",
"esbuild": "^0.25.1",
"prettier": "3.5.3",
- "typescript": "^5.8.2"
+ "typescript": "^5.8.2",
+ "vite": "^6.3.2",
+ "vite-plugin-web-components-hmr": "^0.1.3"
}
},
"node_modules/@ampproject/remapping": {
@@ -888,99 +888,6 @@
"node": ">= 8"
}
},
- "node_modules/@open-wc/dev-server-hmr": {
- "version": "0.1.2-next.0",
- "resolved": "https://registry.npmjs.org/@open-wc/dev-server-hmr/-/dev-server-hmr-0.1.2-next.0.tgz",
- "integrity": "sha512-XgazcRuYE0J17X1LgZ/BumwMf81p7qR1h3ncc3ljA3PDqXIBSOYnt1SSoR/IqlJvDmbTopONYQ/w+qjEOtBrAg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.12.3",
- "@babel/plugin-syntax-class-properties": "^7.12.13",
- "@babel/plugin-syntax-import-assertions": "^7.12.1",
- "@babel/plugin-syntax-top-level-await": "^7.12.1",
- "@web/dev-server-core": "^0.3.10",
- "@web/dev-server-hmr": "^0.1.6",
- "picomatch": "^2.2.2"
- }
- },
- "node_modules/@open-wc/dev-server-hmr/node_modules/@web/dev-server-core": {
- "version": "0.3.19",
- "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.3.19.tgz",
- "integrity": "sha512-Q/Xt4RMVebLWvALofz1C0KvP8qHbzU1EmdIA2Y1WMPJwiFJFhPxdr75p9YxK32P2t0hGs6aqqS5zE0HW9wYzYA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/koa": "^2.11.6",
- "@types/ws": "^7.4.0",
- "@web/parse5-utils": "^1.2.0",
- "chokidar": "^3.4.3",
- "clone": "^2.1.2",
- "es-module-lexer": "^1.0.0",
- "get-stream": "^6.0.0",
- "is-stream": "^2.0.0",
- "isbinaryfile": "^4.0.6",
- "koa": "^2.13.0",
- "koa-etag": "^4.0.0",
- "koa-send": "^5.0.1",
- "koa-static": "^5.0.0",
- "lru-cache": "^6.0.0",
- "mime-types": "^2.1.27",
- "parse5": "^6.0.1",
- "picomatch": "^2.2.2",
- "ws": "^7.4.2"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/@open-wc/dev-server-hmr/node_modules/@web/parse5-utils": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-1.3.1.tgz",
- "integrity": "sha512-haCgDchZrAOB9EhBJ5XqiIjBMsS/exsM5Ru7sCSyNkXVEJWskyyKuKMFk66BonnIGMPpDtqDrTUfYEis5Zi3XA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/parse5": "^6.0.1",
- "parse5": "^6.0.1"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/@open-wc/dev-server-hmr/node_modules/isbinaryfile": {
- "version": "4.0.10",
- "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
- "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/gjtorikian/"
- }
- },
- "node_modules/@open-wc/dev-server-hmr/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@open-wc/dev-server-hmr/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/@playwright/experimental-ct-core": {
"version": "1.51.1",
"resolved": "https://registry.npmjs.org/@playwright/experimental-ct-core/-/experimental-ct-core-1.51.1.tgz",
@@ -1756,83 +1663,6 @@
"url": "https://paulmillr.com/funding/"
}
},
- "node_modules/@web/dev-server-hmr": {
- "version": "0.1.12",
- "resolved": "https://registry.npmjs.org/@web/dev-server-hmr/-/dev-server-hmr-0.1.12.tgz",
- "integrity": "sha512-oqEYVFAh9D74GUigQqxPN5izhocc+A02tZ7Y4QCIHLe6qttjD5R+Hpj8CAObySslfH1X/IGSsWhB8TGctCxlPA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@web/dev-server-core": "^0.4.1"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/@web/dev-server-hmr/node_modules/@web/dev-server-core": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.4.1.tgz",
- "integrity": "sha512-KdYwejXZwIZvb6tYMCqU7yBiEOPfKLQ3V9ezqqEz8DA9V9R3oQWaowckvCpFB9IxxPfS/P8/59OkdzGKQjcIUw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/koa": "^2.11.6",
- "@types/ws": "^7.4.0",
- "@web/parse5-utils": "^1.3.1",
- "chokidar": "^3.4.3",
- "clone": "^2.1.2",
- "es-module-lexer": "^1.0.0",
- "get-stream": "^6.0.0",
- "is-stream": "^2.0.0",
- "isbinaryfile": "^5.0.0",
- "koa": "^2.13.0",
- "koa-etag": "^4.0.0",
- "koa-send": "^5.0.1",
- "koa-static": "^5.0.0",
- "lru-cache": "^6.0.0",
- "mime-types": "^2.1.27",
- "parse5": "^6.0.1",
- "picomatch": "^2.2.2",
- "ws": "^7.4.2"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/@web/dev-server-hmr/node_modules/@web/parse5-utils": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-1.3.1.tgz",
- "integrity": "sha512-haCgDchZrAOB9EhBJ5XqiIjBMsS/exsM5Ru7sCSyNkXVEJWskyyKuKMFk66BonnIGMPpDtqDrTUfYEis5Zi3XA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/parse5": "^6.0.1",
- "parse5": "^6.0.1"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/@web/dev-server-hmr/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@web/dev-server-hmr/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/@web/dev-server-rollup": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/@web/dev-server-rollup/-/dev-server-rollup-0.6.4.tgz",
@@ -2271,20 +2101,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/anymatch": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
- "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- },
- "engines": {
- "node": ">= 8"
- }
- },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2491,19 +2307,6 @@
"node": ">=10.0.0"
}
},
- "node_modules/binary-extensions": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
- "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
@@ -2723,31 +2526,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/chokidar": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
- "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "anymatch": "~3.1.2",
- "braces": "~3.0.2",
- "glob-parent": "~5.1.2",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.6.0"
- },
- "engines": {
- "node": ">= 8.10.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.2"
- }
- },
"node_modules/chrome-launcher": {
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz",
@@ -4432,19 +4210,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "binary-extensions": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@@ -5218,16 +4983,6 @@
"node": "*"
}
},
- "node_modules/normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
@@ -5915,19 +5670,6 @@
"node": ">= 0.8"
}
},
- "node_modules/readdirp": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
- "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "picomatch": "^2.2.1"
- },
- "engines": {
- "node": ">=8.10.0"
- }
- },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -7225,7 +6967,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz",
"integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.3",
@@ -7295,6 +7036,22 @@
}
}
},
+ "node_modules/vite-plugin-web-components-hmr": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/vite-plugin-web-components-hmr/-/vite-plugin-web-components-hmr-0.1.3.tgz",
+ "integrity": "sha512-UF+YYOFyaie6cT7XPatRz2Xo1Fh3TdTwOdCq1Z0EjzMfdAlUiUdF3crkN0DdEhctWBvq18CHxJZUTKw7x1zSRQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.12.3",
+ "@babel/plugin-syntax-class-properties": "^7.12.13",
+ "@babel/plugin-syntax-import-assertions": "^7.12.1",
+ "@babel/plugin-syntax-top-level-await": "^7.12.1",
+ "picomatch": "^2.2.2"
+ },
+ "peerDependencies": {
+ "vite": ">=2"
+ }
+ },
"node_modules/vite/node_modules/fdir": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
diff --git a/loop/webui/package.json b/loop/webui/package.json
index a06362f..262a4fc 100644
--- a/loop/webui/package.json
+++ b/loop/webui/package.json
@@ -11,7 +11,8 @@
},
"scripts": {
"check": "tsc --noEmit",
- "demo": "web-dev-server -config ./web-dev-server.config.mjs --node-resolve --open /src/web-components/demo/",
+ "demo": "vite --open src/web-components/demo/index.html",
+ "dev": "vite",
"format": "prettier ./src --write",
"gentypes": "go run ../../cmd/go2ts -o src/types.ts",
"build": "go run ../../cmd/go2ts -o src/types.ts && tsc",
@@ -30,18 +31,18 @@
"vega-lite": "^5.23.0"
},
"devDependencies": {
- "@open-wc/dev-server-hmr": "^0.1.2-next.0",
"@sand4rt/experimental-ct-web": "^1.51.1",
"@types/marked": "^5.0.2",
"@types/mocha": "^10.0.7",
"@types/node": "^22.13.14",
- "@web/dev-server": "^0.4.6",
"@web/test-runner": "^0.18.2",
"@web/test-runner-puppeteer": "^0.18.0",
"autoprefixer": "^10.4.21",
"esbuild": "^0.25.1",
"prettier": "3.5.3",
- "typescript": "^5.8.2"
+ "typescript": "^5.8.2",
+ "vite": "^6.3.2",
+ "vite-plugin-web-components-hmr": "^0.1.3"
},
"keywords": []
}
diff --git a/loop/webui/readme.md b/loop/webui/readme.md
index b904934..49ca0e8 100644
--- a/loop/webui/readme.md
+++ b/loop/webui/readme.md
@@ -20,9 +20,6 @@
# Install dependencies
make install
-# Build the TypeScript code
-make build
-
# Type checking only
make check
```
@@ -32,10 +29,15 @@
For development, you can use watch mode:
```bash
-make dev
+make demo
```
-This will rebuild the TypeScript files whenever they change.
+This will launch a local web server that serves the demo pages for the web components. You can edit the TypeScript files, and the changes will be reflected in real-time.
+p
+
+#### VSCode
+
+If you are using Visual Studio Code, you can use the `Launch Chrome against localhost` launch configuration to run the demo server. This configuration is set up to automatically open a sketch page with dummy data in Chrome when you start debugging, supporting hot module reloading and breakpoints.
## Integration with Go Server
diff --git a/loop/webui/src/sketch-app-shell.css b/loop/webui/src/sketch-app-shell.css
new file mode 100644
index 0000000..57c96df
--- /dev/null
+++ b/loop/webui/src/sketch-app-shell.css
@@ -0,0 +1,22 @@
+html,
+body {
+ height: 100%;
+ overflow-y: auto;
+}
+
+body {
+ font-family:
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ Roboto,
+ sans-serif;
+ margin: 0;
+ padding: 0;
+ color: #333;
+ line-height: 1.4;
+ overflow-x: hidden; /* Prevent horizontal scrolling */
+ display: flex;
+ flex-direction: column;
+}
diff --git a/loop/webui/src/sketch-app-shell.html b/loop/webui/src/sketch-app-shell.html
index 8d1a30c..c12ce8c 100644
--- a/loop/webui/src/sketch-app-shell.html
+++ b/loop/webui/src/sketch-app-shell.html
@@ -4,30 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>sketch coding assistant</title>
- <!-- We only need basic body styling; all component styles are encapsulated -->
- <style>
- html,
- body {
- height: 100%;
- overflow-y: auto;
- }
- body {
- font-family:
- system-ui,
- -apple-system,
- BlinkMacSystemFont,
- "Segoe UI",
- Roboto,
- sans-serif;
- margin: 0;
- padding: 0;
- color: #333;
- line-height: 1.4;
- overflow-x: hidden; /* Prevent horizontal scrolling */
- display: flex;
- flex-direction: column;
- }
- </style>
+ <link rel="stylesheet" href="sketch-app-shell.css" />
<script src="static/sketch-app-shell.js" async type="module"></script>
</head>
<body>
diff --git a/loop/webui/src/web-components/aggregateAgentMessages.ts b/loop/webui/src/web-components/aggregateAgentMessages.ts
new file mode 100644
index 0000000..2fbd435
--- /dev/null
+++ b/loop/webui/src/web-components/aggregateAgentMessages.ts
@@ -0,0 +1,34 @@
+import { AgentMessage } from "../types";
+
+export function aggregateAgentMessages(
+ arr1: AgentMessage[],
+ arr2: AgentMessage[]): AgentMessage[] {
+ const mergedArray = [...arr1, ...arr2];
+ const seenIds = new Set<number>();
+ const toolCallResults = new Map<string, AgentMessage>();
+
+ let ret: AgentMessage[] = mergedArray
+ .filter((msg) => {
+ if (msg.type == "tool") {
+ toolCallResults.set(msg.tool_call_id, msg);
+ return false;
+ }
+ if (seenIds.has(msg.idx)) {
+ return false; // Skip if idx is already seen
+ }
+
+ seenIds.add(msg.idx);
+ return true;
+ })
+ .sort((a: AgentMessage, b: AgentMessage) => a.idx - b.idx);
+
+ // Attach any tool_call result messages to the original message's tool_call object.
+ ret.forEach((msg) => {
+ msg.tool_calls?.forEach((toolCall) => {
+ if (toolCallResults.has(toolCall.tool_call_id)) {
+ toolCall.result_message = toolCallResults.get(toolCall.tool_call_id);
+ }
+ });
+ });
+ return ret;
+}
diff --git a/loop/webui/src/web-components/demo/readme.md b/loop/webui/src/web-components/demo/readme.md
index 8e3c33c..324d077 100644
--- a/loop/webui/src/web-components/demo/readme.md
+++ b/loop/webui/src/web-components/demo/readme.md
@@ -2,13 +2,4 @@
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.
-# How to use this demo directory to iterate on component development
-
-From the `loop/webui` directory:
-
-1. In one shell, run `npm run watch` to build the web components and watch for changes
-1. In another shell, run `npm run demo` to start a local web server to serve the demo pages
-1. open http://localhost:8000/src/web-components/demo/ in your browser
-1. make edits to the .ts code or to the demo.html files and see how it affects the demo pages in real time
-
-Alternately, use the `webui: watch demo` task in VSCode, which runs all of the above for you.
+See [README](../../../readme.md#development-mode) for more information on how to run the demo pages.
diff --git a/loop/webui/src/web-components/demo/sketch-app-shell.demo.html b/loop/webui/src/web-components/demo/sketch-app-shell.demo.html
index ef335ed..48fc100 100644
--- a/loop/webui/src/web-components/demo/sketch-app-shell.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-app-shell.demo.html
@@ -1,15 +1,13 @@
-<html>
+<!doctype html>
+<html lang="en">
<head>
- <title>sketch-app-shell demo</title>
- <link rel="stylesheet" href="demo.css" />
- <script
- src="/dist/web-components/sketch-app-shell.js"
- type="module"
- ></script>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>sketch coding assistant</title>
+ <link rel="stylesheet" href="sketch-app-shell.css" />
+ <script src="../sketch-app-shell.ts" type="module"></script>
</head>
<body>
- <h1>sketch-app-shell demo</h1>
-
<sketch-app-shell></sketch-app-shell>
</body>
</html>
diff --git a/loop/webui/src/web-components/demo/sketch-charts.demo.html b/loop/webui/src/web-components/demo/sketch-charts.demo.html
index d9b714d..64a9bd2 100644
--- a/loop/webui/src/web-components/demo/sketch-charts.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-charts.demo.html
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<title>Sketch Charts Demo</title>
- <script type="module" src="/dist/web-components/sketch-charts.js"></script>
+ <script type="module" src="../sketch-charts.ts"></script>
<link rel="stylesheet" href="demo.css" />
<style>
sketch-charts {
diff --git a/loop/webui/src/web-components/demo/sketch-chat-input.demo.html b/loop/webui/src/web-components/demo/sketch-chat-input.demo.html
index 99d581b..e76aed7 100644
--- a/loop/webui/src/web-components/demo/sketch-chat-input.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-chat-input.demo.html
@@ -2,9 +2,7 @@
<head>
<title>sketch-chat-input demo</title>
<link rel="stylesheet" href="demo.css" />
- <script
- src="/dist/web-components/sketch-chat-input.js"
- type="module"
+ <script type="module" src="../sketch-chat-input.ts"
></script>
<script>
diff --git a/loop/webui/src/web-components/demo/sketch-container-status.demo.html b/loop/webui/src/web-components/demo/sketch-container-status.demo.html
index a35e881..e18440d 100644
--- a/loop/webui/src/web-components/demo/sketch-container-status.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-container-status.demo.html
@@ -2,9 +2,7 @@
<head>
<title>sketch-container-status demo</title>
<link rel="stylesheet" href="demo.css" />
- <script
- src="/dist/web-components/sketch-container-status.js"
- type="module"
+ <script type="module" src="../sketch-container-status.ts"
></script>
<script>
diff --git a/loop/webui/src/web-components/demo/sketch-diff-view.demo.html b/loop/webui/src/web-components/demo/sketch-diff-view.demo.html
index 3a6cb35..1dc9337 100644
--- a/loop/webui/src/web-components/demo/sketch-diff-view.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-diff-view.demo.html
@@ -9,8 +9,7 @@
href="../../../node_modules/diff2html/bundles/css/diff2html.min.css"
/>
<script
- type="module"
- src="/dist/web-components/sketch-diff-view.js"
+ type="module" src="../sketch-diff-view.ts"
></script>
<style>
body {
diff --git a/loop/webui/src/web-components/demo/sketch-network-status.demo.html b/loop/webui/src/web-components/demo/sketch-network-status.demo.html
index d645840..04c118c 100644
--- a/loop/webui/src/web-components/demo/sketch-network-status.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-network-status.demo.html
@@ -2,9 +2,7 @@
<head>
<title>sketch-network-status demo</title>
<link rel="stylesheet" href="demo.css" />
- <script
- src="/dist/web-components/sketch-network-status.js"
- type="module"
+ <script type="module" src="../sketch-network-status.ts"
></script>
</head>
<body>
diff --git a/loop/webui/src/web-components/demo/sketch-timeline-message.demo.html b/loop/webui/src/web-components/demo/sketch-timeline-message.demo.html
index cb2bdf3..a97145e 100644
--- a/loop/webui/src/web-components/demo/sketch-timeline-message.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-timeline-message.demo.html
@@ -2,9 +2,7 @@
<head>
<title>sketch-timeline-message demo</title>
<link rel="stylesheet" href="demo.css" />
- <script
- src="/dist/web-components/sketch-timeline-message.js"
- type="module"
+ <script type="module" src="../sketch-timeline-message.ts"
></script>
<script>
diff --git a/loop/webui/src/web-components/demo/sketch-timeline.demo.html b/loop/webui/src/web-components/demo/sketch-timeline.demo.html
index be8ab8e..58abdb2 100644
--- a/loop/webui/src/web-components/demo/sketch-timeline.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-timeline.demo.html
@@ -2,9 +2,7 @@
<head>
<title>sketch-timeline demo</title>
<link rel="stylesheet" href="demo.css" />
- <script
- src="/dist/web-components/sketch-timeline.js"
- type="module"
+ <script type="module" src="../sketch-timeline.ts"
></script>
<script>
const messages = [
diff --git a/loop/webui/src/web-components/demo/sketch-tool-calls.demo.html b/loop/webui/src/web-components/demo/sketch-tool-calls.demo.html
index 44b598a..7bedf11 100644
--- a/loop/webui/src/web-components/demo/sketch-tool-calls.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-tool-calls.demo.html
@@ -3,9 +3,7 @@
<title>sketch-tool-calls demo</title>
<link rel="stylesheet" href="demo.css" />
- <script
- src="/dist/web-components/sketch-tool-calls.js"
- type="module"
+ <script type="module" src="../sketch-tool-calls.ts"
></script>
<script>
diff --git a/loop/webui/src/web-components/demo/sketch-tool-card.demo.html b/loop/webui/src/web-components/demo/sketch-tool-card.demo.html
index 17c64ae..3926f2e 100644
--- a/loop/webui/src/web-components/demo/sketch-tool-card.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-tool-card.demo.html
@@ -3,9 +3,7 @@
<title>sketch-tool-card demo</title>
<link rel="stylesheet" href="demo.css" />
- <script
- src="/dist/web-components/sketch-tool-card.js"
- type="module"
+ <script type="module" src="../sketch-tool-card.ts"
></script>
<script>
diff --git a/loop/webui/src/web-components/demo/sketch-view-mode-select.demo.html b/loop/webui/src/web-components/demo/sketch-view-mode-select.demo.html
index 7f795fc..af2f1fb 100644
--- a/loop/webui/src/web-components/demo/sketch-view-mode-select.demo.html
+++ b/loop/webui/src/web-components/demo/sketch-view-mode-select.demo.html
@@ -3,9 +3,7 @@
<title>sketch-view-mode-select demo</title>
<link rel="stylesheet" href="demo.css" />
- <script
- src="/dist/web-components/sketch-view-mode-select.js"
- type="module"
+ <script type="module" src="../sketch-view-mode-select.ts"
></script>
<script>
diff --git a/loop/webui/src/web-components/sketch-app-shell.ts b/loop/webui/src/web-components/sketch-app-shell.ts
index 6ef9232..8f57d75 100644
--- a/loop/webui/src/web-components/sketch-app-shell.ts
+++ b/loop/webui/src/web-components/sketch-app-shell.ts
@@ -11,6 +11,7 @@
import "./sketch-charts";
import "./sketch-terminal";
import { SketchDiffView } from "./sketch-diff-view";
+import { aggregateAgentMessages } from "./aggregateAgentMessages";
type ViewMode = "chat" | "diff" | "charts" | "terminal";
@@ -24,9 +25,6 @@
@state()
currentCommitHash: string = "";
- // Reference to the diff view component
- private diffViewRef?: HTMLElement;
-
// See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
// Note that these styles only apply to the scope of this web component's
// shadow DOM node, so they won't leak out or collide with CSS declared in
@@ -173,7 +171,7 @@
messageStatus: string = "";
// Chat messages
- @property()
+ @property({ attribute: false })
messages: AgentMessage[] = [];
@property()
@@ -184,7 +182,7 @@
private dataManager = new DataManager();
- @property()
+ @property({ attribute: false })
containerState: State = {
title: "",
os: "",
@@ -194,15 +192,12 @@
initial_commit: "",
};
- // Track if this is the first load of messages
- @state()
- private isFirstLoad: boolean = true;
-
// Mutation observer to detect when new messages are added
private mutationObserver: MutationObserver | null = null;
constructor() {
super();
+ console.log("Hello!");
// Binding methods to this
this._handleViewModeSelect = this._handleViewModeSelect.bind(this);
@@ -222,30 +217,21 @@
this.toggleViewMode(mode as ViewMode, false);
// Add popstate event listener to handle browser back/forward navigation
- window.addEventListener("popstate", this._handlePopState as EventListener);
+ window.addEventListener("popstate", this._handlePopState);
// Add event listeners
- window.addEventListener(
- "view-mode-select",
- this._handleViewModeSelect as EventListener,
- );
- window.addEventListener(
- "diff-comment",
- this._handleDiffComment as EventListener,
- );
- window.addEventListener(
- "show-commit-diff",
- this._handleShowCommitDiff as EventListener,
- );
+ window.addEventListener("view-mode-select", this._handleViewModeSelect);
+ window.addEventListener("diff-comment", this._handleDiffComment);
+ window.addEventListener("show-commit-diff", this._handleShowCommitDiff);
// register event listeners
this.dataManager.addEventListener(
"dataChanged",
- this.handleDataChanged.bind(this),
+ this.handleDataChanged.bind(this)
);
this.dataManager.addEventListener(
"connectionStatusChanged",
- this.handleConnectionStatusChanged.bind(this),
+ this.handleConnectionStatusChanged.bind(this)
);
// Initialize the data manager
@@ -255,33 +241,21 @@
// See https://lit.dev/docs/components/lifecycle/
disconnectedCallback() {
super.disconnectedCallback();
- window.removeEventListener(
- "popstate",
- this._handlePopState as EventListener,
- );
+ window.removeEventListener("popstate", this._handlePopState);
// Remove event listeners
- window.removeEventListener(
- "view-mode-select",
- this._handleViewModeSelect as EventListener,
- );
- window.removeEventListener(
- "diff-comment",
- this._handleDiffComment as EventListener,
- );
- window.removeEventListener(
- "show-commit-diff",
- this._handleShowCommitDiff as EventListener,
- );
+ window.removeEventListener("view-mode-select", this._handleViewModeSelect);
+ window.removeEventListener("diff-comment", this._handleDiffComment);
+ window.removeEventListener("show-commit-diff", this._handleShowCommitDiff);
// unregister data manager event listeners
this.dataManager.removeEventListener(
"dataChanged",
- this.handleDataChanged.bind(this),
+ this.handleDataChanged.bind(this)
);
this.dataManager.removeEventListener(
"connectionStatusChanged",
- this.handleConnectionStatusChanged.bind(this),
+ this.handleConnectionStatusChanged.bind(this)
);
// Disconnect mutation observer if it exists
@@ -303,7 +277,7 @@
if (mode !== "chat") {
url.searchParams.set("view", mode);
const diffView = this.shadowRoot?.querySelector(
- ".diff-view",
+ ".diff-view"
) as SketchDiffView;
// If in diff view and there's a commit hash, include that too
@@ -316,7 +290,7 @@
window.history.pushState({ mode }, "", url.toString());
}
- _handlePopState(event) {
+ private _handlePopState(event: PopStateEvent) {
if (event.state && event.state.mode) {
this.toggleViewMode(event.state.mode, false);
} else {
@@ -376,7 +350,7 @@
* Listen for commit diff event
* @param commitHash The commit hash to show diff for
*/
- public showCommitDiff(commitHash: string): void {
+ private showCommitDiff(commitHash: string): void {
// Store the commit hash
this.currentCommitHash = commitHash;
@@ -397,7 +371,7 @@
/**
* Toggle between different view modes: chat, diff, charts, terminal
*/
- public toggleViewMode(mode: ViewMode, updateHistory: boolean): void {
+ private toggleViewMode(mode: ViewMode, updateHistory: boolean): void {
// Don't do anything if the mode is already active
if (this.viewMode === mode) return;
@@ -457,7 +431,7 @@
// Update view mode buttons
const viewModeSelect = this.shadowRoot?.querySelector(
- "sketch-view-mode-select",
+ "sketch-view-mode-select"
);
if (viewModeSelect) {
const event = new CustomEvent("update-active-mode", {
@@ -476,37 +450,6 @@
});
}
- mergeAndDedupe(arr1: AgentMessage[], arr2: AgentMessage[]): AgentMessage[] {
- const mergedArray = [...arr1, ...arr2];
- const seenIds = new Set<number>();
- const toolCallResults = new Map<string, AgentMessage>();
-
- let ret: AgentMessage[] = mergedArray
- .filter((msg) => {
- if (msg.type == "tool") {
- toolCallResults.set(msg.tool_call_id, msg);
- return false;
- }
- if (seenIds.has(msg.idx)) {
- return false; // Skip if idx is already seen
- }
-
- seenIds.add(msg.idx);
- return true;
- })
- .sort((a: AgentMessage, b: AgentMessage) => a.idx - b.idx);
-
- // Attach any tool_call result messages to the original message's tool_call object.
- ret.forEach((msg) => {
- msg.tool_calls?.forEach((toolCall) => {
- if (toolCallResults.has(toolCall.tool_call_id)) {
- toolCall.result_message = toolCallResults.get(toolCall.tool_call_id);
- }
- });
- });
- return ret;
- }
-
private handleDataChanged(eventData: {
state: State;
newMessages: AgentMessage[];
@@ -516,11 +459,8 @@
// Check if this is the first data fetch or if there are new messages
if (isFirstFetch) {
- console.log("Auto-scroll: First data fetch, will scroll to bottom");
- this.isFirstLoad = true;
this.messageStatus = "Initial messages loaded";
} else if (newMessages && newMessages.length > 0) {
- console.log(`Auto-scroll: Received ${newMessages.length} new messages`);
this.messageStatus = "Updated just now";
} else {
this.messageStatus = "No new messages";
@@ -536,19 +476,19 @@
const oldMessageCount = this.messages.length;
// Update messages
- this.messages = this.mergeAndDedupe(this.messages, newMessages);
+ this.messages = aggregateAgentMessages(this.messages, newMessages);
// Log information about the message update
if (this.messages.length > oldMessageCount) {
console.log(
- `Auto-scroll: Messages updated from ${oldMessageCount} to ${this.messages.length}`,
+ `Auto-scroll: Messages updated from ${oldMessageCount} to ${this.messages.length}`
);
}
}
private handleConnectionStatusChanged(
status: ConnectionStatus,
- errorMessage?: string,
+ errorMessage?: string
): void {
this.connectionStatus = status;
this.connectionErrorMessage = errorMessage || "";
@@ -678,11 +618,11 @@
// Initial scroll to bottom when component is first rendered
setTimeout(
() => this.scrollTo({ top: this.scrollHeight, behavior: "smooth" }),
- 50,
+ 50
);
const pollToggleCheckbox = this.renderRoot?.querySelector(
- "#pollToggle",
+ "#pollToggle"
) as HTMLInputElement;
pollToggleCheckbox?.addEventListener("change", () => {
this.dataManager.setPollingEnabled(pollToggleCheckbox.checked);
diff --git a/loop/webui/src/web-components/sketch-chat-input.ts b/loop/webui/src/web-components/sketch-chat-input.ts
index c181724..d5ec75e 100644
--- a/loop/webui/src/web-components/sketch-chat-input.ts
+++ b/loop/webui/src/web-components/sketch-chat-input.ts
@@ -79,7 +79,7 @@
// Update the textarea value directly, otherwise it won't update until next render
const textarea = this.shadowRoot?.querySelector(
- "#chatInput",
+ "#chatInput"
) as HTMLTextAreaElement;
if (textarea) {
textarea.value = content;
@@ -96,7 +96,7 @@
// Listen for update-content events
this.addEventListener(
"update-content",
- this._handleUpdateContent as EventListener,
+ this._handleUpdateContent as EventListener
);
}
@@ -107,7 +107,7 @@
// Remove event listeners
this.removeEventListener(
"update-content",
- this._handleUpdateContent as EventListener,
+ this._handleUpdateContent as EventListener
);
}
diff --git a/loop/webui/src/web-components/sketch-timeline.ts b/loop/webui/src/web-components/sketch-timeline.ts
index b630679..1e63f32 100644
--- a/loop/webui/src/web-components/sketch-timeline.ts
+++ b/loop/webui/src/web-components/sketch-timeline.ts
@@ -7,15 +7,15 @@
@customElement("sketch-timeline")
export class SketchTimeline extends LitElement {
- @property()
+ @property({ attribute: false })
messages: AgentMessage[] = [];
// Track if we should scroll to the bottom
@state()
private scrollingState: "pinToLatest" | "floating" = "pinToLatest";
- @property()
- scrollContainer: HTMLDivElement;
+ @property({ attribute: false })
+ scrollContainer: HTMLElement;
static styles = css`
/* Hide views initially to prevent flash of content */
@@ -142,7 +142,7 @@
Math.abs(
this.scrollContainer.scrollHeight -
this.scrollContainer.clientHeight -
- this.scrollContainer.scrollTop,
+ this.scrollContainer.scrollTop
) <= 1;
if (isAtBottom) {
this.scrollingState = "pinToLatest";
@@ -159,7 +159,7 @@
// Listen for showCommitDiff events from the renderer
document.addEventListener(
"showCommitDiff",
- this._handleShowCommitDiff as EventListener,
+ this._handleShowCommitDiff as EventListener
);
this.scrollContainer?.addEventListener("scroll", this._handleScroll);
}
@@ -171,7 +171,7 @@
// Remove event listeners
document.removeEventListener(
"showCommitDiff",
- this._handleShowCommitDiff as EventListener,
+ this._handleShowCommitDiff as EventListener
);
this.scrollContainer?.removeEventListener("scroll", this._handleScroll);
diff --git a/loop/webui/vite.config.mts b/loop/webui/vite.config.mts
new file mode 100644
index 0000000..74508a5
--- /dev/null
+++ b/loop/webui/vite.config.mts
@@ -0,0 +1,15 @@
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+import { hmrPlugin, presets } from "vite-plugin-web-components-hmr";
+import { defineConfig } from "vite";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+export default defineConfig({
+ plugins: [
+ hmrPlugin({
+ include: ["./src/**/*.ts"],
+ presets: [presets.lit],
+ }),
+ ],
+});
diff --git a/loop/webui/web-dev-server.config.mjs b/loop/webui/web-dev-server.config.mjs
deleted file mode 100644
index a1f598b..0000000
--- a/loop/webui/web-dev-server.config.mjs
+++ /dev/null
@@ -1,13 +0,0 @@
-import { hmrPlugin, presets } from "@open-wc/dev-server-hmr";
-
-export default {
- port: 8000,
- nodeResolve: true,
-
- plugins: [
- hmrPlugin({
- include: ["../**/*"],
- presets: [presets.lit],
- }),
- ],
-};