blob: 41be24ac609cd3f80a919c98966ff457449bfc59 [file] [log] [blame]
Sean McCullough86b56862025-04-18 13:04:03 -07001import { css, html, LitElement } from "lit";
2import { customElement, property, state } from "lit/decorators.js";
Pokey Rule4097e532025-04-24 18:55:28 +01003import { ConnectionStatus, DataManager } from "../data";
4import { AgentMessage, State } from "../types";
Pokey Rulee2a8c2f2025-04-23 15:09:25 +01005import { aggregateAgentMessages } from "./aggregateAgentMessages";
Pokey Rule4097e532025-04-24 18:55:28 +01006import "./sketch-charts";
7import "./sketch-chat-input";
8import "./sketch-container-status";
9import "./sketch-diff-view";
10import { SketchDiffView } from "./sketch-diff-view";
11import "./sketch-network-status";
12import "./sketch-terminal";
13import "./sketch-timeline";
14import "./sketch-view-mode-select";
15
16import { createRef, ref } from "lit/directives/ref.js";
Sean McCullough86b56862025-04-18 13:04:03 -070017
18type ViewMode = "chat" | "diff" | "charts" | "terminal";
19
20@customElement("sketch-app-shell")
21export class SketchAppShell extends LitElement {
22 // Current view mode (chat, diff, charts, terminal)
23 @state()
24 viewMode: "chat" | "diff" | "charts" | "terminal" = "chat";
25
26 // Current commit hash for diff view
27 @state()
28 currentCommitHash: string = "";
29
Sean McCullough86b56862025-04-18 13:04:03 -070030 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
31 // Note that these styles only apply to the scope of this web component's
32 // shadow DOM node, so they won't leak out or collide with CSS declared in
33 // other components or the containing web page (...unless you want it to do that).
34 static styles = css`
35 :host {
36 display: block;
Sean McCullough71941bd2025-04-18 13:31:48 -070037 font-family:
38 system-ui,
39 -apple-system,
40 BlinkMacSystemFont,
41 "Segoe UI",
42 Roboto,
43 sans-serif;
Sean McCullough86b56862025-04-18 13:04:03 -070044 color: #333;
45 line-height: 1.4;
Pokey Rule4097e532025-04-24 18:55:28 +010046 height: 100vh;
Sean McCullough86b56862025-04-18 13:04:03 -070047 width: 100%;
48 position: relative;
49 overflow-x: hidden;
Pokey Rule4097e532025-04-24 18:55:28 +010050 display: flex;
51 flex-direction: column;
Sean McCullough86b56862025-04-18 13:04:03 -070052 }
53
54 /* Top banner with combined elements */
Pokey Rule4097e532025-04-24 18:55:28 +010055 #top-banner {
Sean McCullough86b56862025-04-18 13:04:03 -070056 display: flex;
Pokey Rule4097e532025-04-24 18:55:28 +010057 align-self: flex-start;
Sean McCullough86b56862025-04-18 13:04:03 -070058 justify-content: space-between;
59 align-items: center;
60 padding: 5px 20px;
61 margin-bottom: 0;
62 border-bottom: 1px solid #eee;
63 gap: 10px;
Sean McCullough86b56862025-04-18 13:04:03 -070064 background: white;
Sean McCullough86b56862025-04-18 13:04:03 -070065 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
66 max-width: 100%;
67 }
68
Pokey Rule4097e532025-04-24 18:55:28 +010069 /* View mode container styles - mirroring timeline.css structure */
70 #view-container {
71 align-self: stretch;
72 overflow-y: auto;
73 flex: 1;
74 }
75
76 #view-container-inner {
77 max-width: 1200px;
78 margin: 0 auto;
79 position: relative;
80 padding-bottom: 10px;
81 padding-top: 10px;
82 }
83
84 #chat-input {
85 align-self: flex-end;
86 width: 100%;
87 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
88 }
89
Sean McCullough86b56862025-04-18 13:04:03 -070090 .banner-title {
91 font-size: 18px;
92 font-weight: 600;
93 margin: 0;
94 min-width: 6em;
95 white-space: nowrap;
96 overflow: hidden;
97 text-overflow: ellipsis;
98 }
99
100 .chat-title {
101 margin: 0;
102 padding: 0;
103 color: rgba(82, 82, 82, 0.85);
104 font-size: 16px;
105 font-weight: normal;
106 font-style: italic;
107 white-space: nowrap;
108 overflow: hidden;
109 text-overflow: ellipsis;
110 }
111
Sean McCullough86b56862025-04-18 13:04:03 -0700112 /* Allow the container to expand to full width in diff mode */
Pokey Rule46fff972025-04-25 14:57:44 +0100113 #view-container-inner.diff-active {
Sean McCullough86b56862025-04-18 13:04:03 -0700114 max-width: 100%;
Pokey Rule46fff972025-04-25 14:57:44 +0100115 width: 100%;
Sean McCullough86b56862025-04-18 13:04:03 -0700116 }
117
118 /* Individual view styles */
119 .chat-view,
120 .diff-view,
121 .chart-view,
122 .terminal-view {
123 display: none; /* Hidden by default */
124 width: 100%;
125 }
126
127 /* Active view styles - these will be applied via JavaScript */
128 .view-active {
129 display: flex;
130 flex-direction: column;
131 }
132
133 .title-container {
134 display: flex;
135 flex-direction: column;
136 white-space: nowrap;
137 overflow: hidden;
138 text-overflow: ellipsis;
139 max-width: 33%;
140 }
141
142 .refresh-control {
143 display: flex;
144 align-items: center;
145 margin-bottom: 0;
146 flex-wrap: nowrap;
147 white-space: nowrap;
148 flex-shrink: 0;
149 }
150
151 .refresh-button {
152 background: #4caf50;
153 color: white;
154 border: none;
155 padding: 4px 10px;
156 border-radius: 4px;
157 cursor: pointer;
158 font-size: 12px;
159 margin-right: 5px;
160 }
161
162 .stop-button:hover {
163 background-color: #c82333 !important;
164 }
165
166 .poll-updates {
167 display: flex;
168 align-items: center;
169 margin: 0 5px;
170 font-size: 12px;
171 }
172 `;
173
174 // Header bar: Network connection status details
175 @property()
176 connectionStatus: ConnectionStatus = "disconnected";
177
178 @property()
179 connectionErrorMessage: string = "";
180
181 @property()
182 messageStatus: string = "";
183
184 // Chat messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100185 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700186 messages: AgentMessage[] = [];
Sean McCullough86b56862025-04-18 13:04:03 -0700187
188 @property()
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000189 set title(value: string) {
190 const oldValue = this._title;
191 this._title = value;
192 this.requestUpdate("title", oldValue);
193 // Update document title when title property changes
194 this.updateDocumentTitle();
195 }
196
197 get title(): string {
198 return this._title;
199 }
200
201 private _title: string = "";
Sean McCullough86b56862025-04-18 13:04:03 -0700202
203 private dataManager = new DataManager();
204
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100205 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700206 containerState: State = {
207 title: "",
208 os: "",
209 message_count: 0,
210 hostname: "",
211 working_dir: "",
212 initial_commit: "",
213 };
Sean McCullough86b56862025-04-18 13:04:03 -0700214
Sean McCullough86b56862025-04-18 13:04:03 -0700215 // Mutation observer to detect when new messages are added
216 private mutationObserver: MutationObserver | null = null;
217
218 constructor() {
219 super();
220
221 // Binding methods to this
222 this._handleViewModeSelect = this._handleViewModeSelect.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -0700223 this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
224 this._handlePopState = this._handlePopState.bind(this);
225 }
226
227 // See https://lit.dev/docs/components/lifecycle/
228 connectedCallback() {
229 super.connectedCallback();
230
231 // Initialize client-side nav history.
232 const url = new URL(window.location.href);
233 const mode = url.searchParams.get("view") || "chat";
234 window.history.replaceState({ mode }, "", url.toString());
235
236 this.toggleViewMode(mode as ViewMode, false);
237 // Add popstate event listener to handle browser back/forward navigation
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100238 window.addEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700239
240 // Add event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100241 window.addEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100242 window.addEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700243
244 // register event listeners
245 this.dataManager.addEventListener(
246 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700247 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700248 );
249 this.dataManager.addEventListener(
250 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700251 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700252 );
253
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000254 // Set initial document title
255 this.updateDocumentTitle();
256
Sean McCullough86b56862025-04-18 13:04:03 -0700257 // Initialize the data manager
258 this.dataManager.initialize();
259 }
260
261 // See https://lit.dev/docs/components/lifecycle/
262 disconnectedCallback() {
263 super.disconnectedCallback();
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100264 window.removeEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700265
266 // Remove event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100267 window.removeEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100268 window.removeEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700269
270 // unregister data manager event listeners
271 this.dataManager.removeEventListener(
272 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700273 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700274 );
275 this.dataManager.removeEventListener(
276 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700277 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700278 );
279
280 // Disconnect mutation observer if it exists
281 if (this.mutationObserver) {
Sean McCullough86b56862025-04-18 13:04:03 -0700282 this.mutationObserver.disconnect();
283 this.mutationObserver = null;
284 }
285 }
286
Sean McCullough71941bd2025-04-18 13:31:48 -0700287 updateUrlForViewMode(mode: "chat" | "diff" | "charts" | "terminal"): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700288 // Get the current URL without search parameters
289 const url = new URL(window.location.href);
290
291 // Clear existing parameters
292 url.search = "";
293
294 // Only add view parameter if not in default chat view
295 if (mode !== "chat") {
296 url.searchParams.set("view", mode);
Sean McCullough71941bd2025-04-18 13:31:48 -0700297 const diffView = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700298 ".diff-view",
Sean McCullough71941bd2025-04-18 13:31:48 -0700299 ) as SketchDiffView;
Sean McCullough86b56862025-04-18 13:04:03 -0700300
301 // If in diff view and there's a commit hash, include that too
302 if (mode === "diff" && diffView.commitHash) {
303 url.searchParams.set("commit", diffView.commitHash);
304 }
305 }
306
307 // Update the browser history without reloading the page
308 window.history.pushState({ mode }, "", url.toString());
309 }
310
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100311 private _handlePopState(event: PopStateEvent) {
Sean McCullough86b56862025-04-18 13:04:03 -0700312 if (event.state && event.state.mode) {
313 this.toggleViewMode(event.state.mode, false);
314 } else {
315 this.toggleViewMode("chat", false);
316 }
317 }
318
319 /**
320 * Handle view mode selection event
321 */
322 private _handleViewModeSelect(event: CustomEvent) {
323 const mode = event.detail.mode as "chat" | "diff" | "charts" | "terminal";
324 this.toggleViewMode(mode, true);
325 }
326
327 /**
328 * Handle show commit diff event
329 */
330 private _handleShowCommitDiff(event: CustomEvent) {
331 const { commitHash } = event.detail;
332 if (commitHash) {
333 this.showCommitDiff(commitHash);
334 }
335 }
336
337 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700338 * Listen for commit diff event
339 * @param commitHash The commit hash to show diff for
340 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100341 private showCommitDiff(commitHash: string): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700342 // Store the commit hash
343 this.currentCommitHash = commitHash;
344
345 // Switch to diff view
Sean McCullough71941bd2025-04-18 13:31:48 -0700346 this.toggleViewMode("diff", true);
Sean McCullough86b56862025-04-18 13:04:03 -0700347
348 // Wait for DOM update to complete
349 this.updateComplete.then(() => {
350 // Get the diff view component
351 const diffView = this.shadowRoot?.querySelector("sketch-diff-view");
352 if (diffView) {
353 // Call the showCommitDiff method
354 (diffView as any).showCommitDiff(commitHash);
355 }
356 });
357 }
358
359 /**
360 * Toggle between different view modes: chat, diff, charts, terminal
361 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100362 private toggleViewMode(mode: ViewMode, updateHistory: boolean): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700363 // Don't do anything if the mode is already active
364 if (this.viewMode === mode) return;
365
366 // Update the view mode
367 this.viewMode = mode;
368
369 if (updateHistory) {
370 // Update URL with the current view mode
371 this.updateUrlForViewMode(mode);
372 }
373
374 // Wait for DOM update to complete
375 this.updateComplete.then(() => {
376 // Update active view
Pokey Rule46fff972025-04-25 14:57:44 +0100377 const viewContainerInner = this.shadowRoot?.querySelector(
378 "#view-container-inner",
379 );
Sean McCullough86b56862025-04-18 13:04:03 -0700380 const chatView = this.shadowRoot?.querySelector(".chat-view");
381 const diffView = this.shadowRoot?.querySelector(".diff-view");
382 const chartView = this.shadowRoot?.querySelector(".chart-view");
383 const terminalView = this.shadowRoot?.querySelector(".terminal-view");
384
385 // Remove active class from all views
386 chatView?.classList.remove("view-active");
387 diffView?.classList.remove("view-active");
388 chartView?.classList.remove("view-active");
389 terminalView?.classList.remove("view-active");
390
391 // Add/remove diff-active class on view container
392 if (mode === "diff") {
Pokey Rule46fff972025-04-25 14:57:44 +0100393 viewContainerInner?.classList.add("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700394 } else {
Pokey Rule46fff972025-04-25 14:57:44 +0100395 viewContainerInner?.classList.remove("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700396 }
397
398 // Add active class to the selected view
399 switch (mode) {
400 case "chat":
401 chatView?.classList.add("view-active");
402 break;
403 case "diff":
404 diffView?.classList.add("view-active");
405 // Load diff content if we have a diff view
406 const diffViewComp =
407 this.shadowRoot?.querySelector("sketch-diff-view");
408 if (diffViewComp && this.currentCommitHash) {
409 (diffViewComp as any).showCommitDiff(this.currentCommitHash);
410 } else if (diffViewComp) {
411 (diffViewComp as any).loadDiffContent();
412 }
413 break;
414 case "charts":
415 chartView?.classList.add("view-active");
416 break;
417 case "terminal":
418 terminalView?.classList.add("view-active");
419 break;
420 }
421
422 // Update view mode buttons
423 const viewModeSelect = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700424 "sketch-view-mode-select",
Sean McCullough86b56862025-04-18 13:04:03 -0700425 );
426 if (viewModeSelect) {
427 const event = new CustomEvent("update-active-mode", {
428 detail: { mode },
429 bubbles: true,
430 composed: true,
431 });
432 viewModeSelect.dispatchEvent(event);
433 }
434
435 // FIXME: This is a hack to get vega chart in sketch-charts.ts to work properly
436 // When the chart is in the background, its container has a width of 0, so vega
437 // renders width 0 and only changes that width on a resize event.
438 // See https://github.com/vega/react-vega/issues/85#issuecomment-1826421132
439 window.dispatchEvent(new Event("resize"));
440 });
441 }
442
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000443 /**
444 * Updates the document title based on current title and connection status
445 */
446 private updateDocumentTitle(): void {
447 let docTitle = `sk: ${this.title || "untitled"}`;
448
449 // Add red circle emoji if disconnected
450 if (this.connectionStatus === "disconnected") {
451 docTitle += " 🔴";
452 }
453
454 document.title = docTitle;
455 }
456
Sean McCullough86b56862025-04-18 13:04:03 -0700457 private handleDataChanged(eventData: {
458 state: State;
Sean McCulloughd9f13372025-04-21 15:08:49 -0700459 newMessages: AgentMessage[];
Sean McCullough86b56862025-04-18 13:04:03 -0700460 isFirstFetch?: boolean;
461 }): void {
462 const { state, newMessages, isFirstFetch } = eventData;
463
464 // Check if this is the first data fetch or if there are new messages
465 if (isFirstFetch) {
Sean McCullough86b56862025-04-18 13:04:03 -0700466 this.messageStatus = "Initial messages loaded";
467 } else if (newMessages && newMessages.length > 0) {
Sean McCullough86b56862025-04-18 13:04:03 -0700468 this.messageStatus = "Updated just now";
Sean McCullough86b56862025-04-18 13:04:03 -0700469 } else {
470 this.messageStatus = "No new messages";
471 }
472
473 // Update state if we received it
474 if (state) {
475 this.containerState = state;
476 this.title = state.title;
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000477
478 // Update document title when sketch title changes
479 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700480 }
481
Sean McCullough86b56862025-04-18 13:04:03 -0700482 // Update messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100483 this.messages = aggregateAgentMessages(this.messages, newMessages);
Sean McCullough86b56862025-04-18 13:04:03 -0700484 }
485
486 private handleConnectionStatusChanged(
487 status: ConnectionStatus,
Philip Zeyliger72682df2025-04-23 13:09:46 -0700488 errorMessage?: string,
Sean McCullough86b56862025-04-18 13:04:03 -0700489 ): void {
490 this.connectionStatus = status;
491 this.connectionErrorMessage = errorMessage || "";
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000492
493 // Update document title when connection status changes
494 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700495 }
496
497 async _sendChat(e: CustomEvent) {
498 console.log("app shell: _sendChat", e);
499 const message = e.detail.message?.trim();
500 if (message == "") {
501 return;
502 }
503 try {
504 // Send the message to the server
505 const response = await fetch("chat", {
506 method: "POST",
507 headers: {
508 "Content-Type": "application/json",
509 },
510 body: JSON.stringify({ message }),
511 });
512
513 if (!response.ok) {
514 const errorData = await response.text();
515 throw new Error(`Server error: ${response.status} - ${errorData}`);
516 }
Sean McCullough86b56862025-04-18 13:04:03 -0700517
Philip Zeyliger73db6052025-04-23 13:09:07 -0700518 // TOOD(philip): If the data manager is getting messages out of order, there's a bug?
Sean McCullough86b56862025-04-18 13:04:03 -0700519 // Reset data manager state to force a full refresh after sending a message
520 // This ensures we get all messages in the correct order
521 // Use private API for now - TODO: add a resetState() method to DataManager
522 (this.dataManager as any).nextFetchIndex = 0;
523 (this.dataManager as any).currentFetchStartIndex = 0;
524
Sean McCullough86b56862025-04-18 13:04:03 -0700525 // // If in diff view, switch to conversation view
526 // if (this.viewMode === "diff") {
527 // await this.toggleViewMode("chat");
528 // }
529
530 // Refresh the timeline data to show the new message
531 await this.dataManager.fetchData();
Sean McCullough86b56862025-04-18 13:04:03 -0700532 } catch (error) {
533 console.error("Error sending chat message:", error);
534 const statusText = document.getElementById("statusText");
535 if (statusText) {
536 statusText.textContent = "Error sending message";
537 }
538 }
539 }
540
Pokey Rule4097e532025-04-24 18:55:28 +0100541 private scrollContainerRef = createRef<HTMLElement>();
542
Sean McCullough86b56862025-04-18 13:04:03 -0700543 render() {
544 return html`
Pokey Rule4097e532025-04-24 18:55:28 +0100545 <div id="top-banner">
Sean McCullough86b56862025-04-18 13:04:03 -0700546 <div class="title-container">
547 <h1 class="banner-title">sketch</h1>
548 <h2 id="chatTitle" class="chat-title">${this.title}</h2>
549 </div>
550
551 <sketch-container-status
552 .state=${this.containerState}
553 ></sketch-container-status>
554
555 <div class="refresh-control">
556 <sketch-view-mode-select></sketch-view-mode-select>
557
558 <button id="stopButton" class="refresh-button stop-button">
559 Stop
560 </button>
561
562 <div class="poll-updates">
563 <input type="checkbox" id="pollToggle" checked />
564 <label for="pollToggle">Poll</label>
565 </div>
566
567 <sketch-network-status
568 message=${this.messageStatus}
569 connection=${this.connectionStatus}
570 error=${this.connectionErrorMessage}
571 ></sketch-network-status>
572 </div>
573 </div>
574
Pokey Rule4097e532025-04-24 18:55:28 +0100575 <div id="view-container" ${ref(this.scrollContainerRef)}>
576 <div id="view-container-inner">
577 <div
578 class="chat-view ${this.viewMode === "chat" ? "view-active" : ""}"
579 >
580 <sketch-timeline
581 .messages=${this.messages}
582 .scrollContainer=${this.scrollContainerRef}
583 ></sketch-timeline>
584 </div>
585 <div
586 class="diff-view ${this.viewMode === "diff" ? "view-active" : ""}"
587 >
588 <sketch-diff-view
589 .commitHash=${this.currentCommitHash}
590 ></sketch-diff-view>
591 </div>
592 <div
593 class="chart-view ${this.viewMode === "charts"
594 ? "view-active"
595 : ""}"
596 >
597 <sketch-charts .messages=${this.messages}></sketch-charts>
598 </div>
599 <div
600 class="terminal-view ${this.viewMode === "terminal"
601 ? "view-active"
602 : ""}"
603 >
604 <sketch-terminal></sketch-terminal>
605 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700606 </div>
607 </div>
608
Pokey Rule4097e532025-04-24 18:55:28 +0100609 <div id="chat-input">
610 <sketch-chat-input @send-chat="${this._sendChat}"></sketch-chat-input>
611 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700612 `;
613 }
614
615 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700616 * Lifecycle callback when component is first connected to DOM
617 */
618 firstUpdated(): void {
619 if (this.viewMode !== "chat") {
620 return;
621 }
622
623 // Initial scroll to bottom when component is first rendered
624 setTimeout(
625 () => this.scrollTo({ top: this.scrollHeight, behavior: "smooth" }),
Philip Zeyliger72682df2025-04-23 13:09:46 -0700626 50,
Sean McCullough86b56862025-04-18 13:04:03 -0700627 );
628
Sean McCullough71941bd2025-04-18 13:31:48 -0700629 const pollToggleCheckbox = this.renderRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700630 "#pollToggle",
Sean McCullough71941bd2025-04-18 13:31:48 -0700631 ) as HTMLInputElement;
Sean McCullough86b56862025-04-18 13:04:03 -0700632 pollToggleCheckbox?.addEventListener("change", () => {
633 this.dataManager.setPollingEnabled(pollToggleCheckbox.checked);
634 if (!pollToggleCheckbox.checked) {
635 this.connectionStatus = "disabled";
636 this.messageStatus = "Polling stopped";
637 } else {
638 this.messageStatus = "Polling for updates...";
639 }
640 });
641 }
642}
643
644declare global {
645 interface HTMLElementTagNameMap {
646 "sketch-app-shell": SketchAppShell;
647 }
648}