blob: 6ef92326c87272b7ddcbff1b1069f47f688a12b1 [file] [log] [blame]
Sean McCullough86b56862025-04-18 13:04:03 -07001import { css, html, LitElement } from "lit";
2import { customElement, property, state } from "lit/decorators.js";
Sean McCullough86b56862025-04-18 13:04:03 -07003import { DataManager, ConnectionStatus } from "../data";
Sean McCulloughd9f13372025-04-21 15:08:49 -07004import { State, AgentMessage } from "../types";
Sean McCullough86b56862025-04-18 13:04:03 -07005import "./sketch-container-status";
6import "./sketch-view-mode-select";
7import "./sketch-network-status";
8import "./sketch-timeline";
9import "./sketch-chat-input";
10import "./sketch-diff-view";
11import "./sketch-charts";
12import "./sketch-terminal";
13import { SketchDiffView } from "./sketch-diff-view";
Sean McCullough86b56862025-04-18 13:04:03 -070014
15type ViewMode = "chat" | "diff" | "charts" | "terminal";
16
17@customElement("sketch-app-shell")
18export class SketchAppShell extends LitElement {
19 // Current view mode (chat, diff, charts, terminal)
20 @state()
21 viewMode: "chat" | "diff" | "charts" | "terminal" = "chat";
22
23 // Current commit hash for diff view
24 @state()
25 currentCommitHash: string = "";
26
27 // Reference to the diff view component
28 private diffViewRef?: HTMLElement;
29
30 // 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;
46 min-height: 100vh;
47 width: 100%;
48 position: relative;
49 overflow-x: hidden;
50 }
51
52 /* Top banner with combined elements */
53 .top-banner {
54 display: flex;
55 justify-content: space-between;
56 align-items: center;
57 padding: 5px 20px;
58 margin-bottom: 0;
59 border-bottom: 1px solid #eee;
60 gap: 10px;
61 position: fixed;
62 top: 0;
63 left: 0;
64 right: 0;
65 background: white;
66 z-index: 100;
67 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
68 max-width: 100%;
69 }
70
71 .banner-title {
72 font-size: 18px;
73 font-weight: 600;
74 margin: 0;
75 min-width: 6em;
76 white-space: nowrap;
77 overflow: hidden;
78 text-overflow: ellipsis;
79 }
80
81 .chat-title {
82 margin: 0;
83 padding: 0;
84 color: rgba(82, 82, 82, 0.85);
85 font-size: 16px;
86 font-weight: normal;
87 font-style: italic;
88 white-space: nowrap;
89 overflow: hidden;
90 text-overflow: ellipsis;
91 }
92
93 /* View mode container styles - mirroring timeline.css structure */
94 .view-container {
95 max-width: 1200px;
96 margin: 0 auto;
97 margin-top: 65px; /* Space for the top banner */
98 margin-bottom: 90px; /* Increased space for the chat input */
99 position: relative;
100 padding-bottom: 15px; /* Additional padding to prevent clipping */
101 padding-top: 15px; /* Add padding at top to prevent content touching the header */
102 }
103
104 /* Allow the container to expand to full width in diff mode */
105 .view-container.diff-active {
106 max-width: 100%;
107 }
108
109 /* Individual view styles */
110 .chat-view,
111 .diff-view,
112 .chart-view,
113 .terminal-view {
114 display: none; /* Hidden by default */
115 width: 100%;
116 }
117
118 /* Active view styles - these will be applied via JavaScript */
119 .view-active {
120 display: flex;
121 flex-direction: column;
122 }
123
124 .title-container {
125 display: flex;
126 flex-direction: column;
127 white-space: nowrap;
128 overflow: hidden;
129 text-overflow: ellipsis;
130 max-width: 33%;
131 }
132
133 .refresh-control {
134 display: flex;
135 align-items: center;
136 margin-bottom: 0;
137 flex-wrap: nowrap;
138 white-space: nowrap;
139 flex-shrink: 0;
140 }
141
142 .refresh-button {
143 background: #4caf50;
144 color: white;
145 border: none;
146 padding: 4px 10px;
147 border-radius: 4px;
148 cursor: pointer;
149 font-size: 12px;
150 margin-right: 5px;
151 }
152
153 .stop-button:hover {
154 background-color: #c82333 !important;
155 }
156
157 .poll-updates {
158 display: flex;
159 align-items: center;
160 margin: 0 5px;
161 font-size: 12px;
162 }
163 `;
164
165 // Header bar: Network connection status details
166 @property()
167 connectionStatus: ConnectionStatus = "disconnected";
168
169 @property()
170 connectionErrorMessage: string = "";
171
172 @property()
173 messageStatus: string = "";
174
175 // Chat messages
176 @property()
Sean McCulloughd9f13372025-04-21 15:08:49 -0700177 messages: AgentMessage[] = [];
Sean McCullough86b56862025-04-18 13:04:03 -0700178
179 @property()
180 chatMessageText: string = "";
181
182 @property()
183 title: string = "";
184
185 private dataManager = new DataManager();
186
187 @property()
Sean McCulloughd9f13372025-04-21 15:08:49 -0700188 containerState: State = {
189 title: "",
190 os: "",
191 message_count: 0,
192 hostname: "",
193 working_dir: "",
194 initial_commit: "",
195 };
Sean McCullough86b56862025-04-18 13:04:03 -0700196
197 // Track if this is the first load of messages
198 @state()
199 private isFirstLoad: boolean = true;
200
Sean McCullough86b56862025-04-18 13:04:03 -0700201 // Mutation observer to detect when new messages are added
202 private mutationObserver: MutationObserver | null = null;
203
204 constructor() {
205 super();
206
207 // Binding methods to this
208 this._handleViewModeSelect = this._handleViewModeSelect.bind(this);
209 this._handleDiffComment = this._handleDiffComment.bind(this);
210 this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
211 this._handlePopState = this._handlePopState.bind(this);
212 }
213
214 // See https://lit.dev/docs/components/lifecycle/
215 connectedCallback() {
216 super.connectedCallback();
217
218 // Initialize client-side nav history.
219 const url = new URL(window.location.href);
220 const mode = url.searchParams.get("view") || "chat";
221 window.history.replaceState({ mode }, "", url.toString());
222
223 this.toggleViewMode(mode as ViewMode, false);
224 // Add popstate event listener to handle browser back/forward navigation
Sean McCullough71941bd2025-04-18 13:31:48 -0700225 window.addEventListener("popstate", this._handlePopState as EventListener);
Sean McCullough86b56862025-04-18 13:04:03 -0700226
227 // Add event listeners
228 window.addEventListener(
229 "view-mode-select",
Sean McCullough71941bd2025-04-18 13:31:48 -0700230 this._handleViewModeSelect as EventListener,
Sean McCullough86b56862025-04-18 13:04:03 -0700231 );
232 window.addEventListener(
233 "diff-comment",
Sean McCullough71941bd2025-04-18 13:31:48 -0700234 this._handleDiffComment as EventListener,
Sean McCullough86b56862025-04-18 13:04:03 -0700235 );
236 window.addEventListener(
237 "show-commit-diff",
Sean McCullough71941bd2025-04-18 13:31:48 -0700238 this._handleShowCommitDiff as EventListener,
Sean McCullough86b56862025-04-18 13:04:03 -0700239 );
240
241 // register event listeners
242 this.dataManager.addEventListener(
243 "dataChanged",
Sean McCullough71941bd2025-04-18 13:31:48 -0700244 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700245 );
246 this.dataManager.addEventListener(
247 "connectionStatusChanged",
Sean McCullough71941bd2025-04-18 13:31:48 -0700248 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700249 );
250
251 // Initialize the data manager
252 this.dataManager.initialize();
253 }
254
255 // See https://lit.dev/docs/components/lifecycle/
256 disconnectedCallback() {
257 super.disconnectedCallback();
258 window.removeEventListener(
259 "popstate",
Sean McCullough71941bd2025-04-18 13:31:48 -0700260 this._handlePopState as EventListener,
261 );
Sean McCullough86b56862025-04-18 13:04:03 -0700262
263 // Remove event listeners
264 window.removeEventListener(
265 "view-mode-select",
Sean McCullough71941bd2025-04-18 13:31:48 -0700266 this._handleViewModeSelect as EventListener,
Sean McCullough86b56862025-04-18 13:04:03 -0700267 );
268 window.removeEventListener(
269 "diff-comment",
Sean McCullough71941bd2025-04-18 13:31:48 -0700270 this._handleDiffComment as EventListener,
Sean McCullough86b56862025-04-18 13:04:03 -0700271 );
272 window.removeEventListener(
273 "show-commit-diff",
Sean McCullough71941bd2025-04-18 13:31:48 -0700274 this._handleShowCommitDiff as EventListener,
Sean McCullough86b56862025-04-18 13:04:03 -0700275 );
276
277 // unregister data manager event listeners
278 this.dataManager.removeEventListener(
279 "dataChanged",
Sean McCullough71941bd2025-04-18 13:31:48 -0700280 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700281 );
282 this.dataManager.removeEventListener(
283 "connectionStatusChanged",
Sean McCullough71941bd2025-04-18 13:31:48 -0700284 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700285 );
286
287 // Disconnect mutation observer if it exists
288 if (this.mutationObserver) {
289 console.log("Auto-scroll: Disconnecting mutation observer");
290 this.mutationObserver.disconnect();
291 this.mutationObserver = null;
292 }
293 }
294
Sean McCullough71941bd2025-04-18 13:31:48 -0700295 updateUrlForViewMode(mode: "chat" | "diff" | "charts" | "terminal"): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700296 // Get the current URL without search parameters
297 const url = new URL(window.location.href);
298
299 // Clear existing parameters
300 url.search = "";
301
302 // Only add view parameter if not in default chat view
303 if (mode !== "chat") {
304 url.searchParams.set("view", mode);
Sean McCullough71941bd2025-04-18 13:31:48 -0700305 const diffView = this.shadowRoot?.querySelector(
306 ".diff-view",
307 ) as SketchDiffView;
Sean McCullough86b56862025-04-18 13:04:03 -0700308
309 // If in diff view and there's a commit hash, include that too
310 if (mode === "diff" && diffView.commitHash) {
311 url.searchParams.set("commit", diffView.commitHash);
312 }
313 }
314
315 // Update the browser history without reloading the page
316 window.history.pushState({ mode }, "", url.toString());
317 }
318
319 _handlePopState(event) {
320 if (event.state && event.state.mode) {
321 this.toggleViewMode(event.state.mode, false);
322 } else {
323 this.toggleViewMode("chat", false);
324 }
325 }
326
327 /**
328 * Handle view mode selection event
329 */
330 private _handleViewModeSelect(event: CustomEvent) {
331 const mode = event.detail.mode as "chat" | "diff" | "charts" | "terminal";
332 this.toggleViewMode(mode, true);
333 }
334
335 /**
336 * Handle show commit diff event
337 */
338 private _handleShowCommitDiff(event: CustomEvent) {
339 const { commitHash } = event.detail;
340 if (commitHash) {
341 this.showCommitDiff(commitHash);
342 }
343 }
344
345 /**
346 * Handle diff comment event
347 */
348 private _handleDiffComment(event: CustomEvent) {
349 const { comment } = event.detail;
350 if (!comment) return;
351
352 // Find the chat input textarea
353 const chatInput = this.shadowRoot?.querySelector("sketch-chat-input");
354 if (chatInput) {
355 // Update the chat input content using property
356 const currentContent = chatInput.getAttribute("content") || "";
357 const newContent = currentContent
358 ? `${currentContent}\n\n${comment}`
359 : comment;
360 chatInput.setAttribute("content", newContent);
361
362 // Dispatch an event to update the textarea value in the chat input component
363 const updateEvent = new CustomEvent("update-content", {
364 detail: { content: newContent },
365 bubbles: true,
366 composed: true,
367 });
368 chatInput.dispatchEvent(updateEvent);
369
370 // Switch back to chat view
371 this.toggleViewMode("chat", true);
372 }
373 }
374
375 /**
376 * Listen for commit diff event
377 * @param commitHash The commit hash to show diff for
378 */
379 public showCommitDiff(commitHash: string): void {
380 // Store the commit hash
381 this.currentCommitHash = commitHash;
382
383 // Switch to diff view
Sean McCullough71941bd2025-04-18 13:31:48 -0700384 this.toggleViewMode("diff", true);
Sean McCullough86b56862025-04-18 13:04:03 -0700385
386 // Wait for DOM update to complete
387 this.updateComplete.then(() => {
388 // Get the diff view component
389 const diffView = this.shadowRoot?.querySelector("sketch-diff-view");
390 if (diffView) {
391 // Call the showCommitDiff method
392 (diffView as any).showCommitDiff(commitHash);
393 }
394 });
395 }
396
397 /**
398 * Toggle between different view modes: chat, diff, charts, terminal
399 */
400 public toggleViewMode(mode: ViewMode, updateHistory: boolean): void {
401 // Don't do anything if the mode is already active
402 if (this.viewMode === mode) return;
403
404 // Update the view mode
405 this.viewMode = mode;
406
407 if (updateHistory) {
408 // Update URL with the current view mode
409 this.updateUrlForViewMode(mode);
410 }
411
412 // Wait for DOM update to complete
413 this.updateComplete.then(() => {
414 // Update active view
415 const viewContainer = this.shadowRoot?.querySelector(".view-container");
416 const chatView = this.shadowRoot?.querySelector(".chat-view");
417 const diffView = this.shadowRoot?.querySelector(".diff-view");
418 const chartView = this.shadowRoot?.querySelector(".chart-view");
419 const terminalView = this.shadowRoot?.querySelector(".terminal-view");
420
421 // Remove active class from all views
422 chatView?.classList.remove("view-active");
423 diffView?.classList.remove("view-active");
424 chartView?.classList.remove("view-active");
425 terminalView?.classList.remove("view-active");
426
427 // Add/remove diff-active class on view container
428 if (mode === "diff") {
429 viewContainer?.classList.add("diff-active");
430 } else {
431 viewContainer?.classList.remove("diff-active");
432 }
433
434 // Add active class to the selected view
435 switch (mode) {
436 case "chat":
437 chatView?.classList.add("view-active");
438 break;
439 case "diff":
440 diffView?.classList.add("view-active");
441 // Load diff content if we have a diff view
442 const diffViewComp =
443 this.shadowRoot?.querySelector("sketch-diff-view");
444 if (diffViewComp && this.currentCommitHash) {
445 (diffViewComp as any).showCommitDiff(this.currentCommitHash);
446 } else if (diffViewComp) {
447 (diffViewComp as any).loadDiffContent();
448 }
449 break;
450 case "charts":
451 chartView?.classList.add("view-active");
452 break;
453 case "terminal":
454 terminalView?.classList.add("view-active");
455 break;
456 }
457
458 // Update view mode buttons
459 const viewModeSelect = this.shadowRoot?.querySelector(
Sean McCullough71941bd2025-04-18 13:31:48 -0700460 "sketch-view-mode-select",
Sean McCullough86b56862025-04-18 13:04:03 -0700461 );
462 if (viewModeSelect) {
463 const event = new CustomEvent("update-active-mode", {
464 detail: { mode },
465 bubbles: true,
466 composed: true,
467 });
468 viewModeSelect.dispatchEvent(event);
469 }
470
471 // FIXME: This is a hack to get vega chart in sketch-charts.ts to work properly
472 // When the chart is in the background, its container has a width of 0, so vega
473 // renders width 0 and only changes that width on a resize event.
474 // See https://github.com/vega/react-vega/issues/85#issuecomment-1826421132
475 window.dispatchEvent(new Event("resize"));
476 });
477 }
478
Sean McCulloughd9f13372025-04-21 15:08:49 -0700479 mergeAndDedupe(arr1: AgentMessage[], arr2: AgentMessage[]): AgentMessage[] {
Sean McCullough86b56862025-04-18 13:04:03 -0700480 const mergedArray = [...arr1, ...arr2];
481 const seenIds = new Set<number>();
Sean McCulloughd9f13372025-04-21 15:08:49 -0700482 const toolCallResults = new Map<string, AgentMessage>();
Sean McCullough86b56862025-04-18 13:04:03 -0700483
Sean McCulloughd9f13372025-04-21 15:08:49 -0700484 let ret: AgentMessage[] = mergedArray
Sean McCullough86b56862025-04-18 13:04:03 -0700485 .filter((msg) => {
486 if (msg.type == "tool") {
487 toolCallResults.set(msg.tool_call_id, msg);
488 return false;
489 }
490 if (seenIds.has(msg.idx)) {
491 return false; // Skip if idx is already seen
492 }
493
494 seenIds.add(msg.idx);
495 return true;
496 })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700497 .sort((a: AgentMessage, b: AgentMessage) => a.idx - b.idx);
Sean McCullough86b56862025-04-18 13:04:03 -0700498
499 // Attach any tool_call result messages to the original message's tool_call object.
500 ret.forEach((msg) => {
501 msg.tool_calls?.forEach((toolCall) => {
502 if (toolCallResults.has(toolCall.tool_call_id)) {
503 toolCall.result_message = toolCallResults.get(toolCall.tool_call_id);
504 }
505 });
506 });
507 return ret;
508 }
509
510 private handleDataChanged(eventData: {
511 state: State;
Sean McCulloughd9f13372025-04-21 15:08:49 -0700512 newMessages: AgentMessage[];
Sean McCullough86b56862025-04-18 13:04:03 -0700513 isFirstFetch?: boolean;
514 }): void {
515 const { state, newMessages, isFirstFetch } = eventData;
516
517 // Check if this is the first data fetch or if there are new messages
518 if (isFirstFetch) {
519 console.log("Auto-scroll: First data fetch, will scroll to bottom");
520 this.isFirstLoad = true;
Sean McCullough86b56862025-04-18 13:04:03 -0700521 this.messageStatus = "Initial messages loaded";
522 } else if (newMessages && newMessages.length > 0) {
523 console.log(`Auto-scroll: Received ${newMessages.length} new messages`);
524 this.messageStatus = "Updated just now";
Sean McCullough86b56862025-04-18 13:04:03 -0700525 } else {
526 this.messageStatus = "No new messages";
527 }
528
529 // Update state if we received it
530 if (state) {
531 this.containerState = state;
532 this.title = state.title;
533 }
534
535 // Create a copy of the current messages before updating
536 const oldMessageCount = this.messages.length;
537
538 // Update messages
539 this.messages = this.mergeAndDedupe(this.messages, newMessages);
540
541 // Log information about the message update
542 if (this.messages.length > oldMessageCount) {
543 console.log(
Sean McCullough2c5bba42025-04-20 19:33:17 -0700544 `Auto-scroll: Messages updated from ${oldMessageCount} to ${this.messages.length}`,
Sean McCullough86b56862025-04-18 13:04:03 -0700545 );
546 }
547 }
548
549 private handleConnectionStatusChanged(
550 status: ConnectionStatus,
Sean McCullough71941bd2025-04-18 13:31:48 -0700551 errorMessage?: string,
Sean McCullough86b56862025-04-18 13:04:03 -0700552 ): void {
553 this.connectionStatus = status;
554 this.connectionErrorMessage = errorMessage || "";
555 }
556
557 async _sendChat(e: CustomEvent) {
558 console.log("app shell: _sendChat", e);
559 const message = e.detail.message?.trim();
560 if (message == "") {
561 return;
562 }
563 try {
564 // Send the message to the server
565 const response = await fetch("chat", {
566 method: "POST",
567 headers: {
568 "Content-Type": "application/json",
569 },
570 body: JSON.stringify({ message }),
571 });
572
573 if (!response.ok) {
574 const errorData = await response.text();
575 throw new Error(`Server error: ${response.status} - ${errorData}`);
576 }
577 // Clear the input after successfully sending the message.
578 this.chatMessageText = "";
579
580 // Reset data manager state to force a full refresh after sending a message
581 // This ensures we get all messages in the correct order
582 // Use private API for now - TODO: add a resetState() method to DataManager
583 (this.dataManager as any).nextFetchIndex = 0;
584 (this.dataManager as any).currentFetchStartIndex = 0;
585
Sean McCullough86b56862025-04-18 13:04:03 -0700586 // // If in diff view, switch to conversation view
587 // if (this.viewMode === "diff") {
588 // await this.toggleViewMode("chat");
589 // }
590
591 // Refresh the timeline data to show the new message
592 await this.dataManager.fetchData();
Sean McCullough86b56862025-04-18 13:04:03 -0700593 } catch (error) {
594 console.error("Error sending chat message:", error);
595 const statusText = document.getElementById("statusText");
596 if (statusText) {
597 statusText.textContent = "Error sending message";
598 }
599 }
600 }
601
602 render() {
603 return html`
604 <div class="top-banner">
605 <div class="title-container">
606 <h1 class="banner-title">sketch</h1>
607 <h2 id="chatTitle" class="chat-title">${this.title}</h2>
608 </div>
609
610 <sketch-container-status
611 .state=${this.containerState}
612 ></sketch-container-status>
613
614 <div class="refresh-control">
615 <sketch-view-mode-select></sketch-view-mode-select>
616
617 <button id="stopButton" class="refresh-button stop-button">
618 Stop
619 </button>
620
621 <div class="poll-updates">
622 <input type="checkbox" id="pollToggle" checked />
623 <label for="pollToggle">Poll</label>
624 </div>
625
626 <sketch-network-status
627 message=${this.messageStatus}
628 connection=${this.connectionStatus}
629 error=${this.connectionErrorMessage}
630 ></sketch-network-status>
631 </div>
632 </div>
633
634 <div class="view-container">
635 <div class="chat-view ${this.viewMode === "chat" ? "view-active" : ""}">
Sean McCullough2c5bba42025-04-20 19:33:17 -0700636 <sketch-timeline
637 .messages=${this.messages}
638 .scrollContainer=${this}
639 ></sketch-timeline>
Sean McCullough86b56862025-04-18 13:04:03 -0700640 </div>
641
642 <div class="diff-view ${this.viewMode === "diff" ? "view-active" : ""}">
643 <sketch-diff-view
644 .commitHash=${this.currentCommitHash}
645 ></sketch-diff-view>
646 </div>
647
648 <div
649 class="chart-view ${this.viewMode === "charts" ? "view-active" : ""}"
650 >
651 <sketch-charts .messages=${this.messages}></sketch-charts>
652 </div>
653
654 <div
655 class="terminal-view ${this.viewMode === "terminal"
656 ? "view-active"
657 : ""}"
658 >
659 <sketch-terminal></sketch-terminal>
660 </div>
661 </div>
662
663 <sketch-chat-input
664 .content=${this.chatMessageText}
665 @send-chat="${this._sendChat}"
666 ></sketch-chat-input>
667 `;
668 }
669
670 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700671 * Lifecycle callback when component is first connected to DOM
672 */
673 firstUpdated(): void {
674 if (this.viewMode !== "chat") {
675 return;
676 }
677
678 // Initial scroll to bottom when component is first rendered
679 setTimeout(
680 () => this.scrollTo({ top: this.scrollHeight, behavior: "smooth" }),
Sean McCullough71941bd2025-04-18 13:31:48 -0700681 50,
Sean McCullough86b56862025-04-18 13:04:03 -0700682 );
683
Sean McCullough71941bd2025-04-18 13:31:48 -0700684 const pollToggleCheckbox = this.renderRoot?.querySelector(
685 "#pollToggle",
686 ) as HTMLInputElement;
Sean McCullough86b56862025-04-18 13:04:03 -0700687 pollToggleCheckbox?.addEventListener("change", () => {
688 this.dataManager.setPollingEnabled(pollToggleCheckbox.checked);
689 if (!pollToggleCheckbox.checked) {
690 this.connectionStatus = "disabled";
691 this.messageStatus = "Polling stopped";
692 } else {
693 this.messageStatus = "Polling for updates...";
694 }
695 });
696 }
697}
698
699declare global {
700 interface HTMLElementTagNameMap {
701 "sketch-app-shell": SketchAppShell;
702 }
703}