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/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);