blob: 40e551247a3f960c170194c16b6f1ad1200417e7 [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";
Philip Zeyliger99a9a022025-04-27 15:15:25 +000012import "./sketch-call-status";
Pokey Rule4097e532025-04-24 18:55:28 +010013import "./sketch-terminal";
14import "./sketch-timeline";
15import "./sketch-view-mode-select";
16
17import { createRef, ref } from "lit/directives/ref.js";
Sean McCullough86b56862025-04-18 13:04:03 -070018
19type ViewMode = "chat" | "diff" | "charts" | "terminal";
20
21@customElement("sketch-app-shell")
22export class SketchAppShell extends LitElement {
23 // Current view mode (chat, diff, charts, terminal)
24 @state()
25 viewMode: "chat" | "diff" | "charts" | "terminal" = "chat";
26
27 // Current commit hash for diff view
28 @state()
29 currentCommitHash: string = "";
30
Sean McCullough86b56862025-04-18 13:04:03 -070031 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
32 // Note that these styles only apply to the scope of this web component's
33 // shadow DOM node, so they won't leak out or collide with CSS declared in
34 // other components or the containing web page (...unless you want it to do that).
35 static styles = css`
36 :host {
37 display: block;
Sean McCullough71941bd2025-04-18 13:31:48 -070038 font-family:
39 system-ui,
40 -apple-system,
41 BlinkMacSystemFont,
42 "Segoe UI",
43 Roboto,
44 sans-serif;
Sean McCullough86b56862025-04-18 13:04:03 -070045 color: #333;
46 line-height: 1.4;
Pokey Rule4097e532025-04-24 18:55:28 +010047 height: 100vh;
Sean McCullough86b56862025-04-18 13:04:03 -070048 width: 100%;
49 position: relative;
50 overflow-x: hidden;
Pokey Rule4097e532025-04-24 18:55:28 +010051 display: flex;
52 flex-direction: column;
Sean McCullough86b56862025-04-18 13:04:03 -070053 }
54
55 /* Top banner with combined elements */
Pokey Rule4097e532025-04-24 18:55:28 +010056 #top-banner {
Sean McCullough86b56862025-04-18 13:04:03 -070057 display: flex;
Pokey Rule4097e532025-04-24 18:55:28 +010058 align-self: flex-start;
Sean McCullough86b56862025-04-18 13:04:03 -070059 justify-content: space-between;
60 align-items: center;
61 padding: 5px 20px;
62 margin-bottom: 0;
63 border-bottom: 1px solid #eee;
64 gap: 10px;
Sean McCullough86b56862025-04-18 13:04:03 -070065 background: white;
Sean McCullough86b56862025-04-18 13:04:03 -070066 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
67 max-width: 100%;
68 }
69
Pokey Rule4097e532025-04-24 18:55:28 +010070 /* View mode container styles - mirroring timeline.css structure */
71 #view-container {
72 align-self: stretch;
73 overflow-y: auto;
74 flex: 1;
75 }
76
77 #view-container-inner {
78 max-width: 1200px;
79 margin: 0 auto;
80 position: relative;
81 padding-bottom: 10px;
82 padding-top: 10px;
83 }
84
85 #chat-input {
86 align-self: flex-end;
87 width: 100%;
88 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
89 }
90
Sean McCullough86b56862025-04-18 13:04:03 -070091 .banner-title {
92 font-size: 18px;
93 font-weight: 600;
94 margin: 0;
95 min-width: 6em;
96 white-space: nowrap;
97 overflow: hidden;
98 text-overflow: ellipsis;
99 }
100
101 .chat-title {
102 margin: 0;
103 padding: 0;
104 color: rgba(82, 82, 82, 0.85);
105 font-size: 16px;
106 font-weight: normal;
107 font-style: italic;
108 white-space: nowrap;
109 overflow: hidden;
110 text-overflow: ellipsis;
111 }
112
Sean McCullough86b56862025-04-18 13:04:03 -0700113 /* Allow the container to expand to full width in diff mode */
Pokey Rule46fff972025-04-25 14:57:44 +0100114 #view-container-inner.diff-active {
Sean McCullough86b56862025-04-18 13:04:03 -0700115 max-width: 100%;
Pokey Rule46fff972025-04-25 14:57:44 +0100116 width: 100%;
Sean McCullough86b56862025-04-18 13:04:03 -0700117 }
118
119 /* Individual view styles */
120 .chat-view,
121 .diff-view,
122 .chart-view,
123 .terminal-view {
124 display: none; /* Hidden by default */
125 width: 100%;
126 }
127
128 /* Active view styles - these will be applied via JavaScript */
129 .view-active {
130 display: flex;
131 flex-direction: column;
132 }
133
134 .title-container {
135 display: flex;
136 flex-direction: column;
137 white-space: nowrap;
138 overflow: hidden;
139 text-overflow: ellipsis;
140 max-width: 33%;
141 }
142
143 .refresh-control {
144 display: flex;
145 align-items: center;
146 margin-bottom: 0;
147 flex-wrap: nowrap;
148 white-space: nowrap;
149 flex-shrink: 0;
150 }
151
152 .refresh-button {
153 background: #4caf50;
154 color: white;
155 border: none;
156 padding: 4px 10px;
157 border-radius: 4px;
158 cursor: pointer;
159 font-size: 12px;
160 margin-right: 5px;
161 }
162
163 .stop-button:hover {
164 background-color: #c82333 !important;
165 }
166
167 .poll-updates {
168 display: flex;
169 align-items: center;
170 margin: 0 5px;
171 font-size: 12px;
172 }
173 `;
174
175 // Header bar: Network connection status details
176 @property()
177 connectionStatus: ConnectionStatus = "disconnected";
178
179 @property()
180 connectionErrorMessage: string = "";
181
182 @property()
183 messageStatus: string = "";
184
185 // Chat messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100186 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700187 messages: AgentMessage[] = [];
Sean McCullough86b56862025-04-18 13:04:03 -0700188
189 @property()
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000190 set title(value: string) {
191 const oldValue = this._title;
192 this._title = value;
193 this.requestUpdate("title", oldValue);
194 // Update document title when title property changes
195 this.updateDocumentTitle();
196 }
197
198 get title(): string {
199 return this._title;
200 }
201
202 private _title: string = "";
Sean McCullough86b56862025-04-18 13:04:03 -0700203
204 private dataManager = new DataManager();
205
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100206 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700207 containerState: State = {
208 title: "",
209 os: "",
210 message_count: 0,
211 hostname: "",
212 working_dir: "",
213 initial_commit: "",
Philip Zeyliger99a9a022025-04-27 15:15:25 +0000214 outstanding_llm_calls: 0,
215 outstanding_tool_calls: [],
Sean McCulloughd9f13372025-04-21 15:08:49 -0700216 };
Sean McCullough86b56862025-04-18 13:04:03 -0700217
Sean McCullough86b56862025-04-18 13:04:03 -0700218 // Mutation observer to detect when new messages are added
219 private mutationObserver: MutationObserver | null = null;
220
221 constructor() {
222 super();
223
224 // Binding methods to this
225 this._handleViewModeSelect = this._handleViewModeSelect.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -0700226 this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
227 this._handlePopState = this._handlePopState.bind(this);
228 }
229
230 // See https://lit.dev/docs/components/lifecycle/
231 connectedCallback() {
232 super.connectedCallback();
233
234 // Initialize client-side nav history.
235 const url = new URL(window.location.href);
236 const mode = url.searchParams.get("view") || "chat";
237 window.history.replaceState({ mode }, "", url.toString());
238
239 this.toggleViewMode(mode as ViewMode, false);
240 // Add popstate event listener to handle browser back/forward navigation
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100241 window.addEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700242
243 // Add event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100244 window.addEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100245 window.addEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700246
247 // register event listeners
248 this.dataManager.addEventListener(
249 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700250 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700251 );
252 this.dataManager.addEventListener(
253 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700254 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700255 );
256
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000257 // Set initial document title
258 this.updateDocumentTitle();
259
Sean McCullough86b56862025-04-18 13:04:03 -0700260 // Initialize the data manager
261 this.dataManager.initialize();
262 }
263
264 // See https://lit.dev/docs/components/lifecycle/
265 disconnectedCallback() {
266 super.disconnectedCallback();
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100267 window.removeEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700268
269 // Remove event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100270 window.removeEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100271 window.removeEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700272
273 // unregister data manager event listeners
274 this.dataManager.removeEventListener(
275 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700276 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700277 );
278 this.dataManager.removeEventListener(
279 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700280 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700281 );
282
283 // Disconnect mutation observer if it exists
284 if (this.mutationObserver) {
Sean McCullough86b56862025-04-18 13:04:03 -0700285 this.mutationObserver.disconnect();
286 this.mutationObserver = null;
287 }
288 }
289
Sean McCullough71941bd2025-04-18 13:31:48 -0700290 updateUrlForViewMode(mode: "chat" | "diff" | "charts" | "terminal"): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700291 // Get the current URL without search parameters
292 const url = new URL(window.location.href);
293
294 // Clear existing parameters
295 url.search = "";
296
297 // Only add view parameter if not in default chat view
298 if (mode !== "chat") {
299 url.searchParams.set("view", mode);
Sean McCullough71941bd2025-04-18 13:31:48 -0700300 const diffView = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700301 ".diff-view",
Sean McCullough71941bd2025-04-18 13:31:48 -0700302 ) as SketchDiffView;
Sean McCullough86b56862025-04-18 13:04:03 -0700303
304 // If in diff view and there's a commit hash, include that too
305 if (mode === "diff" && diffView.commitHash) {
306 url.searchParams.set("commit", diffView.commitHash);
307 }
308 }
309
310 // Update the browser history without reloading the page
311 window.history.pushState({ mode }, "", url.toString());
312 }
313
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100314 private _handlePopState(event: PopStateEvent) {
Sean McCullough86b56862025-04-18 13:04:03 -0700315 if (event.state && event.state.mode) {
316 this.toggleViewMode(event.state.mode, false);
317 } else {
318 this.toggleViewMode("chat", false);
319 }
320 }
321
322 /**
323 * Handle view mode selection event
324 */
325 private _handleViewModeSelect(event: CustomEvent) {
326 const mode = event.detail.mode as "chat" | "diff" | "charts" | "terminal";
327 this.toggleViewMode(mode, true);
328 }
329
330 /**
331 * Handle show commit diff event
332 */
333 private _handleShowCommitDiff(event: CustomEvent) {
334 const { commitHash } = event.detail;
335 if (commitHash) {
336 this.showCommitDiff(commitHash);
337 }
338 }
339
340 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700341 * Listen for commit diff event
342 * @param commitHash The commit hash to show diff for
343 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100344 private showCommitDiff(commitHash: string): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700345 // Store the commit hash
346 this.currentCommitHash = commitHash;
347
348 // Switch to diff view
Sean McCullough71941bd2025-04-18 13:31:48 -0700349 this.toggleViewMode("diff", true);
Sean McCullough86b56862025-04-18 13:04:03 -0700350
351 // Wait for DOM update to complete
352 this.updateComplete.then(() => {
353 // Get the diff view component
354 const diffView = this.shadowRoot?.querySelector("sketch-diff-view");
355 if (diffView) {
356 // Call the showCommitDiff method
357 (diffView as any).showCommitDiff(commitHash);
358 }
359 });
360 }
361
362 /**
363 * Toggle between different view modes: chat, diff, charts, terminal
364 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100365 private toggleViewMode(mode: ViewMode, updateHistory: boolean): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700366 // Don't do anything if the mode is already active
367 if (this.viewMode === mode) return;
368
369 // Update the view mode
370 this.viewMode = mode;
371
372 if (updateHistory) {
373 // Update URL with the current view mode
374 this.updateUrlForViewMode(mode);
375 }
376
377 // Wait for DOM update to complete
378 this.updateComplete.then(() => {
379 // Update active view
Pokey Rule46fff972025-04-25 14:57:44 +0100380 const viewContainerInner = this.shadowRoot?.querySelector(
381 "#view-container-inner",
382 );
Sean McCullough86b56862025-04-18 13:04:03 -0700383 const chatView = this.shadowRoot?.querySelector(".chat-view");
384 const diffView = this.shadowRoot?.querySelector(".diff-view");
385 const chartView = this.shadowRoot?.querySelector(".chart-view");
386 const terminalView = this.shadowRoot?.querySelector(".terminal-view");
387
388 // Remove active class from all views
389 chatView?.classList.remove("view-active");
390 diffView?.classList.remove("view-active");
391 chartView?.classList.remove("view-active");
392 terminalView?.classList.remove("view-active");
393
394 // Add/remove diff-active class on view container
395 if (mode === "diff") {
Pokey Rule46fff972025-04-25 14:57:44 +0100396 viewContainerInner?.classList.add("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700397 } else {
Pokey Rule46fff972025-04-25 14:57:44 +0100398 viewContainerInner?.classList.remove("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700399 }
400
401 // Add active class to the selected view
402 switch (mode) {
403 case "chat":
404 chatView?.classList.add("view-active");
405 break;
406 case "diff":
407 diffView?.classList.add("view-active");
408 // Load diff content if we have a diff view
409 const diffViewComp =
410 this.shadowRoot?.querySelector("sketch-diff-view");
411 if (diffViewComp && this.currentCommitHash) {
412 (diffViewComp as any).showCommitDiff(this.currentCommitHash);
413 } else if (diffViewComp) {
414 (diffViewComp as any).loadDiffContent();
415 }
416 break;
417 case "charts":
418 chartView?.classList.add("view-active");
419 break;
420 case "terminal":
421 terminalView?.classList.add("view-active");
422 break;
423 }
424
425 // Update view mode buttons
426 const viewModeSelect = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700427 "sketch-view-mode-select",
Sean McCullough86b56862025-04-18 13:04:03 -0700428 );
429 if (viewModeSelect) {
430 const event = new CustomEvent("update-active-mode", {
431 detail: { mode },
432 bubbles: true,
433 composed: true,
434 });
435 viewModeSelect.dispatchEvent(event);
436 }
437
438 // FIXME: This is a hack to get vega chart in sketch-charts.ts to work properly
439 // When the chart is in the background, its container has a width of 0, so vega
440 // renders width 0 and only changes that width on a resize event.
441 // See https://github.com/vega/react-vega/issues/85#issuecomment-1826421132
442 window.dispatchEvent(new Event("resize"));
443 });
444 }
445
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000446 /**
447 * Updates the document title based on current title and connection status
448 */
449 private updateDocumentTitle(): void {
450 let docTitle = `sk: ${this.title || "untitled"}`;
451
452 // Add red circle emoji if disconnected
453 if (this.connectionStatus === "disconnected") {
454 docTitle += " 🔴";
455 }
456
457 document.title = docTitle;
458 }
459
Sean McCullough86b56862025-04-18 13:04:03 -0700460 private handleDataChanged(eventData: {
461 state: State;
Sean McCulloughd9f13372025-04-21 15:08:49 -0700462 newMessages: AgentMessage[];
Sean McCullough86b56862025-04-18 13:04:03 -0700463 isFirstFetch?: boolean;
464 }): void {
465 const { state, newMessages, isFirstFetch } = eventData;
466
467 // Check if this is the first data fetch or if there are new messages
468 if (isFirstFetch) {
Sean McCullough86b56862025-04-18 13:04:03 -0700469 this.messageStatus = "Initial messages loaded";
470 } else if (newMessages && newMessages.length > 0) {
Sean McCullough86b56862025-04-18 13:04:03 -0700471 this.messageStatus = "Updated just now";
Sean McCullough86b56862025-04-18 13:04:03 -0700472 } else {
473 this.messageStatus = "No new messages";
474 }
475
476 // Update state if we received it
477 if (state) {
478 this.containerState = state;
479 this.title = state.title;
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000480
481 // Update document title when sketch title changes
482 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700483 }
484
Sean McCullough86b56862025-04-18 13:04:03 -0700485 // Update messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100486 this.messages = aggregateAgentMessages(this.messages, newMessages);
Sean McCullough86b56862025-04-18 13:04:03 -0700487 }
488
489 private handleConnectionStatusChanged(
490 status: ConnectionStatus,
Philip Zeyliger72682df2025-04-23 13:09:46 -0700491 errorMessage?: string,
Sean McCullough86b56862025-04-18 13:04:03 -0700492 ): void {
493 this.connectionStatus = status;
494 this.connectionErrorMessage = errorMessage || "";
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000495
496 // Update document title when connection status changes
497 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700498 }
499
500 async _sendChat(e: CustomEvent) {
501 console.log("app shell: _sendChat", e);
502 const message = e.detail.message?.trim();
503 if (message == "") {
504 return;
505 }
506 try {
507 // Send the message to the server
508 const response = await fetch("chat", {
509 method: "POST",
510 headers: {
511 "Content-Type": "application/json",
512 },
513 body: JSON.stringify({ message }),
514 });
515
516 if (!response.ok) {
517 const errorData = await response.text();
518 throw new Error(`Server error: ${response.status} - ${errorData}`);
519 }
Sean McCullough86b56862025-04-18 13:04:03 -0700520
Philip Zeyliger73db6052025-04-23 13:09:07 -0700521 // TOOD(philip): If the data manager is getting messages out of order, there's a bug?
Sean McCullough86b56862025-04-18 13:04:03 -0700522 // Reset data manager state to force a full refresh after sending a message
523 // This ensures we get all messages in the correct order
524 // Use private API for now - TODO: add a resetState() method to DataManager
525 (this.dataManager as any).nextFetchIndex = 0;
526 (this.dataManager as any).currentFetchStartIndex = 0;
527
Sean McCullough86b56862025-04-18 13:04:03 -0700528 // // If in diff view, switch to conversation view
529 // if (this.viewMode === "diff") {
530 // await this.toggleViewMode("chat");
531 // }
532
533 // Refresh the timeline data to show the new message
534 await this.dataManager.fetchData();
Sean McCullough86b56862025-04-18 13:04:03 -0700535 } catch (error) {
536 console.error("Error sending chat message:", error);
537 const statusText = document.getElementById("statusText");
538 if (statusText) {
539 statusText.textContent = "Error sending message";
540 }
541 }
542 }
543
Pokey Rule4097e532025-04-24 18:55:28 +0100544 private scrollContainerRef = createRef<HTMLElement>();
545
Sean McCullough86b56862025-04-18 13:04:03 -0700546 render() {
547 return html`
Pokey Rule4097e532025-04-24 18:55:28 +0100548 <div id="top-banner">
Sean McCullough86b56862025-04-18 13:04:03 -0700549 <div class="title-container">
550 <h1 class="banner-title">sketch</h1>
551 <h2 id="chatTitle" class="chat-title">${this.title}</h2>
552 </div>
553
554 <sketch-container-status
555 .state=${this.containerState}
556 ></sketch-container-status>
557
558 <div class="refresh-control">
559 <sketch-view-mode-select></sketch-view-mode-select>
560
561 <button id="stopButton" class="refresh-button stop-button">
562 Stop
563 </button>
564
565 <div class="poll-updates">
566 <input type="checkbox" id="pollToggle" checked />
567 <label for="pollToggle">Poll</label>
568 </div>
569
570 <sketch-network-status
571 message=${this.messageStatus}
572 connection=${this.connectionStatus}
573 error=${this.connectionErrorMessage}
574 ></sketch-network-status>
Philip Zeyliger99a9a022025-04-27 15:15:25 +0000575
576 <sketch-call-status
577 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
578 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
579 ></sketch-call-status>
Sean McCullough86b56862025-04-18 13:04:03 -0700580 </div>
581 </div>
582
Pokey Rule4097e532025-04-24 18:55:28 +0100583 <div id="view-container" ${ref(this.scrollContainerRef)}>
584 <div id="view-container-inner">
585 <div
586 class="chat-view ${this.viewMode === "chat" ? "view-active" : ""}"
587 >
588 <sketch-timeline
589 .messages=${this.messages}
590 .scrollContainer=${this.scrollContainerRef}
591 ></sketch-timeline>
592 </div>
593 <div
594 class="diff-view ${this.viewMode === "diff" ? "view-active" : ""}"
595 >
596 <sketch-diff-view
597 .commitHash=${this.currentCommitHash}
598 ></sketch-diff-view>
599 </div>
600 <div
601 class="chart-view ${this.viewMode === "charts"
602 ? "view-active"
603 : ""}"
604 >
605 <sketch-charts .messages=${this.messages}></sketch-charts>
606 </div>
607 <div
608 class="terminal-view ${this.viewMode === "terminal"
609 ? "view-active"
610 : ""}"
611 >
612 <sketch-terminal></sketch-terminal>
613 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700614 </div>
615 </div>
616
Pokey Rule4097e532025-04-24 18:55:28 +0100617 <div id="chat-input">
618 <sketch-chat-input @send-chat="${this._sendChat}"></sketch-chat-input>
619 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700620 `;
621 }
622
623 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700624 * Lifecycle callback when component is first connected to DOM
625 */
626 firstUpdated(): void {
627 if (this.viewMode !== "chat") {
628 return;
629 }
630
631 // Initial scroll to bottom when component is first rendered
632 setTimeout(
633 () => this.scrollTo({ top: this.scrollHeight, behavior: "smooth" }),
Philip Zeyliger72682df2025-04-23 13:09:46 -0700634 50,
Sean McCullough86b56862025-04-18 13:04:03 -0700635 );
636
Sean McCullough71941bd2025-04-18 13:31:48 -0700637 const pollToggleCheckbox = this.renderRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700638 "#pollToggle",
Sean McCullough71941bd2025-04-18 13:31:48 -0700639 ) as HTMLInputElement;
Sean McCullough86b56862025-04-18 13:04:03 -0700640 pollToggleCheckbox?.addEventListener("change", () => {
641 this.dataManager.setPollingEnabled(pollToggleCheckbox.checked);
642 if (!pollToggleCheckbox.checked) {
643 this.connectionStatus = "disabled";
644 this.messageStatus = "Polling stopped";
645 } else {
646 this.messageStatus = "Polling for updates...";
647 }
648 });
649 }
650}
651
652declare global {
653 interface HTMLElementTagNameMap {
654 "sketch-app-shell": SketchAppShell;
655 }
656}