blob: bb05a03fb2ac4904188b33b05e4690ae28aee706 [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;
Philip Zeyligere66db3e2025-04-27 15:40:39 +000058 align-self: stretch;
Sean McCullough86b56862025-04-18 13:04:03 -070059 justify-content: space-between;
60 align-items: center;
Philip Zeyligere66db3e2025-04-27 15:40:39 +000061 padding: 0 20px;
Sean McCullough86b56862025-04-18 13:04:03 -070062 margin-bottom: 0;
63 border-bottom: 1px solid #eee;
Philip Zeyligere66db3e2025-04-27 15:40:39 +000064 gap: 20px;
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);
Philip Zeyligere66db3e2025-04-27 15:40:39 +000067 width: 100%;
68 height: 48px;
69 padding-right: 30px; /* Extra padding on the right to prevent elements from hitting the edge */
Sean McCullough86b56862025-04-18 13:04:03 -070070 }
71
Pokey Rule4097e532025-04-24 18:55:28 +010072 /* View mode container styles - mirroring timeline.css structure */
73 #view-container {
74 align-self: stretch;
75 overflow-y: auto;
76 flex: 1;
77 }
78
79 #view-container-inner {
80 max-width: 1200px;
81 margin: 0 auto;
82 position: relative;
83 padding-bottom: 10px;
84 padding-top: 10px;
85 }
86
87 #chat-input {
88 align-self: flex-end;
89 width: 100%;
90 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
91 }
92
Sean McCullough86b56862025-04-18 13:04:03 -070093 .banner-title {
94 font-size: 18px;
95 font-weight: 600;
96 margin: 0;
97 min-width: 6em;
98 white-space: nowrap;
99 overflow: hidden;
100 text-overflow: ellipsis;
101 }
102
103 .chat-title {
104 margin: 0;
105 padding: 0;
106 color: rgba(82, 82, 82, 0.85);
107 font-size: 16px;
108 font-weight: normal;
109 font-style: italic;
110 white-space: nowrap;
111 overflow: hidden;
112 text-overflow: ellipsis;
113 }
114
Sean McCullough86b56862025-04-18 13:04:03 -0700115 /* Allow the container to expand to full width in diff mode */
Pokey Rule46fff972025-04-25 14:57:44 +0100116 #view-container-inner.diff-active {
Sean McCullough86b56862025-04-18 13:04:03 -0700117 max-width: 100%;
Pokey Rule46fff972025-04-25 14:57:44 +0100118 width: 100%;
Sean McCullough86b56862025-04-18 13:04:03 -0700119 }
120
121 /* Individual view styles */
122 .chat-view,
123 .diff-view,
124 .chart-view,
125 .terminal-view {
126 display: none; /* Hidden by default */
127 width: 100%;
128 }
129
130 /* Active view styles - these will be applied via JavaScript */
131 .view-active {
132 display: flex;
133 flex-direction: column;
134 }
135
136 .title-container {
137 display: flex;
138 flex-direction: column;
139 white-space: nowrap;
140 overflow: hidden;
141 text-overflow: ellipsis;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000142 max-width: 25%;
143 padding: 6px 0;
Sean McCullough86b56862025-04-18 13:04:03 -0700144 }
145
146 .refresh-control {
147 display: flex;
148 align-items: center;
149 margin-bottom: 0;
150 flex-wrap: nowrap;
151 white-space: nowrap;
152 flex-shrink: 0;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000153 gap: 15px;
154 padding-left: 15px;
155 margin-right: 50px;
Sean McCullough86b56862025-04-18 13:04:03 -0700156 }
157
158 .refresh-button {
159 background: #4caf50;
160 color: white;
161 border: none;
162 padding: 4px 10px;
163 border-radius: 4px;
164 cursor: pointer;
165 font-size: 12px;
166 margin-right: 5px;
167 }
168
169 .stop-button:hover {
170 background-color: #c82333 !important;
171 }
172
173 .poll-updates {
174 display: flex;
175 align-items: center;
Sean McCullough86b56862025-04-18 13:04:03 -0700176 font-size: 12px;
177 }
178 `;
179
180 // Header bar: Network connection status details
181 @property()
182 connectionStatus: ConnectionStatus = "disconnected";
183
184 @property()
185 connectionErrorMessage: string = "";
186
187 @property()
188 messageStatus: string = "";
189
190 // Chat messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100191 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700192 messages: AgentMessage[] = [];
Sean McCullough86b56862025-04-18 13:04:03 -0700193
194 @property()
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000195 set title(value: string) {
196 const oldValue = this._title;
197 this._title = value;
198 this.requestUpdate("title", oldValue);
199 // Update document title when title property changes
200 this.updateDocumentTitle();
201 }
202
203 get title(): string {
204 return this._title;
205 }
206
207 private _title: string = "";
Sean McCullough86b56862025-04-18 13:04:03 -0700208
209 private dataManager = new DataManager();
210
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100211 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700212 containerState: State = {
213 title: "",
214 os: "",
215 message_count: 0,
216 hostname: "",
217 working_dir: "",
218 initial_commit: "",
Philip Zeyliger99a9a022025-04-27 15:15:25 +0000219 outstanding_llm_calls: 0,
220 outstanding_tool_calls: [],
Sean McCulloughd9f13372025-04-21 15:08:49 -0700221 };
Sean McCullough86b56862025-04-18 13:04:03 -0700222
Sean McCullough86b56862025-04-18 13:04:03 -0700223 // Mutation observer to detect when new messages are added
224 private mutationObserver: MutationObserver | null = null;
225
226 constructor() {
227 super();
228
229 // Binding methods to this
230 this._handleViewModeSelect = this._handleViewModeSelect.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -0700231 this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
232 this._handlePopState = this._handlePopState.bind(this);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000233 this._handleStopClick = this._handleStopClick.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -0700234 }
235
236 // See https://lit.dev/docs/components/lifecycle/
237 connectedCallback() {
238 super.connectedCallback();
239
240 // Initialize client-side nav history.
241 const url = new URL(window.location.href);
242 const mode = url.searchParams.get("view") || "chat";
243 window.history.replaceState({ mode }, "", url.toString());
244
245 this.toggleViewMode(mode as ViewMode, false);
246 // Add popstate event listener to handle browser back/forward navigation
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100247 window.addEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700248
249 // Add event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100250 window.addEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100251 window.addEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700252
253 // register event listeners
254 this.dataManager.addEventListener(
255 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700256 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700257 );
258 this.dataManager.addEventListener(
259 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700260 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700261 );
262
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000263 // Set initial document title
264 this.updateDocumentTitle();
265
Sean McCullough86b56862025-04-18 13:04:03 -0700266 // Initialize the data manager
267 this.dataManager.initialize();
268 }
269
270 // See https://lit.dev/docs/components/lifecycle/
271 disconnectedCallback() {
272 super.disconnectedCallback();
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100273 window.removeEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700274
275 // Remove event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100276 window.removeEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100277 window.removeEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700278
279 // unregister data manager event listeners
280 this.dataManager.removeEventListener(
281 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700282 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700283 );
284 this.dataManager.removeEventListener(
285 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700286 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700287 );
288
289 // Disconnect mutation observer if it exists
290 if (this.mutationObserver) {
Sean McCullough86b56862025-04-18 13:04:03 -0700291 this.mutationObserver.disconnect();
292 this.mutationObserver = null;
293 }
294 }
295
Sean McCullough71941bd2025-04-18 13:31:48 -0700296 updateUrlForViewMode(mode: "chat" | "diff" | "charts" | "terminal"): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700297 // Get the current URL without search parameters
298 const url = new URL(window.location.href);
299
300 // Clear existing parameters
301 url.search = "";
302
303 // Only add view parameter if not in default chat view
304 if (mode !== "chat") {
305 url.searchParams.set("view", mode);
Sean McCullough71941bd2025-04-18 13:31:48 -0700306 const diffView = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700307 ".diff-view",
Sean McCullough71941bd2025-04-18 13:31:48 -0700308 ) as SketchDiffView;
Sean McCullough86b56862025-04-18 13:04:03 -0700309
310 // If in diff view and there's a commit hash, include that too
311 if (mode === "diff" && diffView.commitHash) {
312 url.searchParams.set("commit", diffView.commitHash);
313 }
314 }
315
316 // Update the browser history without reloading the page
317 window.history.pushState({ mode }, "", url.toString());
318 }
319
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100320 private _handlePopState(event: PopStateEvent) {
Sean McCullough86b56862025-04-18 13:04:03 -0700321 if (event.state && event.state.mode) {
322 this.toggleViewMode(event.state.mode, false);
323 } else {
324 this.toggleViewMode("chat", false);
325 }
326 }
327
328 /**
329 * Handle view mode selection event
330 */
331 private _handleViewModeSelect(event: CustomEvent) {
332 const mode = event.detail.mode as "chat" | "diff" | "charts" | "terminal";
333 this.toggleViewMode(mode, true);
334 }
335
336 /**
337 * Handle show commit diff event
338 */
339 private _handleShowCommitDiff(event: CustomEvent) {
340 const { commitHash } = event.detail;
341 if (commitHash) {
342 this.showCommitDiff(commitHash);
343 }
344 }
345
346 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700347 * Listen for commit diff event
348 * @param commitHash The commit hash to show diff for
349 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100350 private showCommitDiff(commitHash: string): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700351 // Store the commit hash
352 this.currentCommitHash = commitHash;
353
354 // Switch to diff view
Sean McCullough71941bd2025-04-18 13:31:48 -0700355 this.toggleViewMode("diff", true);
Sean McCullough86b56862025-04-18 13:04:03 -0700356
357 // Wait for DOM update to complete
358 this.updateComplete.then(() => {
359 // Get the diff view component
360 const diffView = this.shadowRoot?.querySelector("sketch-diff-view");
361 if (diffView) {
362 // Call the showCommitDiff method
363 (diffView as any).showCommitDiff(commitHash);
364 }
365 });
366 }
367
368 /**
369 * Toggle between different view modes: chat, diff, charts, terminal
370 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100371 private toggleViewMode(mode: ViewMode, updateHistory: boolean): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700372 // Don't do anything if the mode is already active
373 if (this.viewMode === mode) return;
374
375 // Update the view mode
376 this.viewMode = mode;
377
378 if (updateHistory) {
379 // Update URL with the current view mode
380 this.updateUrlForViewMode(mode);
381 }
382
383 // Wait for DOM update to complete
384 this.updateComplete.then(() => {
385 // Update active view
Pokey Rule46fff972025-04-25 14:57:44 +0100386 const viewContainerInner = this.shadowRoot?.querySelector(
387 "#view-container-inner",
388 );
Sean McCullough86b56862025-04-18 13:04:03 -0700389 const chatView = this.shadowRoot?.querySelector(".chat-view");
390 const diffView = this.shadowRoot?.querySelector(".diff-view");
391 const chartView = this.shadowRoot?.querySelector(".chart-view");
392 const terminalView = this.shadowRoot?.querySelector(".terminal-view");
393
394 // Remove active class from all views
395 chatView?.classList.remove("view-active");
396 diffView?.classList.remove("view-active");
397 chartView?.classList.remove("view-active");
398 terminalView?.classList.remove("view-active");
399
400 // Add/remove diff-active class on view container
401 if (mode === "diff") {
Pokey Rule46fff972025-04-25 14:57:44 +0100402 viewContainerInner?.classList.add("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700403 } else {
Pokey Rule46fff972025-04-25 14:57:44 +0100404 viewContainerInner?.classList.remove("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700405 }
406
407 // Add active class to the selected view
408 switch (mode) {
409 case "chat":
410 chatView?.classList.add("view-active");
411 break;
412 case "diff":
413 diffView?.classList.add("view-active");
414 // Load diff content if we have a diff view
415 const diffViewComp =
416 this.shadowRoot?.querySelector("sketch-diff-view");
417 if (diffViewComp && this.currentCommitHash) {
418 (diffViewComp as any).showCommitDiff(this.currentCommitHash);
419 } else if (diffViewComp) {
420 (diffViewComp as any).loadDiffContent();
421 }
422 break;
423 case "charts":
424 chartView?.classList.add("view-active");
425 break;
426 case "terminal":
427 terminalView?.classList.add("view-active");
428 break;
429 }
430
431 // Update view mode buttons
432 const viewModeSelect = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700433 "sketch-view-mode-select",
Sean McCullough86b56862025-04-18 13:04:03 -0700434 );
435 if (viewModeSelect) {
436 const event = new CustomEvent("update-active-mode", {
437 detail: { mode },
438 bubbles: true,
439 composed: true,
440 });
441 viewModeSelect.dispatchEvent(event);
442 }
443
444 // FIXME: This is a hack to get vega chart in sketch-charts.ts to work properly
445 // When the chart is in the background, its container has a width of 0, so vega
446 // renders width 0 and only changes that width on a resize event.
447 // See https://github.com/vega/react-vega/issues/85#issuecomment-1826421132
448 window.dispatchEvent(new Event("resize"));
449 });
450 }
451
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000452 /**
453 * Updates the document title based on current title and connection status
454 */
455 private updateDocumentTitle(): void {
456 let docTitle = `sk: ${this.title || "untitled"}`;
457
458 // Add red circle emoji if disconnected
459 if (this.connectionStatus === "disconnected") {
460 docTitle += " 🔴";
461 }
462
463 document.title = docTitle;
464 }
465
Sean McCullough86b56862025-04-18 13:04:03 -0700466 private handleDataChanged(eventData: {
467 state: State;
Sean McCulloughd9f13372025-04-21 15:08:49 -0700468 newMessages: AgentMessage[];
Sean McCullough86b56862025-04-18 13:04:03 -0700469 isFirstFetch?: boolean;
470 }): void {
471 const { state, newMessages, isFirstFetch } = eventData;
472
473 // Check if this is the first data fetch or if there are new messages
474 if (isFirstFetch) {
Sean McCullough86b56862025-04-18 13:04:03 -0700475 this.messageStatus = "Initial messages loaded";
476 } else if (newMessages && newMessages.length > 0) {
Sean McCullough86b56862025-04-18 13:04:03 -0700477 this.messageStatus = "Updated just now";
Sean McCullough86b56862025-04-18 13:04:03 -0700478 } else {
479 this.messageStatus = "No new messages";
480 }
481
482 // Update state if we received it
483 if (state) {
484 this.containerState = state;
485 this.title = state.title;
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000486
487 // Update document title when sketch title changes
488 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700489 }
490
Sean McCullough86b56862025-04-18 13:04:03 -0700491 // Update messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100492 this.messages = aggregateAgentMessages(this.messages, newMessages);
Sean McCullough86b56862025-04-18 13:04:03 -0700493 }
494
495 private handleConnectionStatusChanged(
496 status: ConnectionStatus,
Philip Zeyliger72682df2025-04-23 13:09:46 -0700497 errorMessage?: string,
Sean McCullough86b56862025-04-18 13:04:03 -0700498 ): void {
499 this.connectionStatus = status;
500 this.connectionErrorMessage = errorMessage || "";
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000501
502 // Update document title when connection status changes
503 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700504 }
505
Sean McCulloughd3906e22025-04-29 17:32:14 +0000506 /**
507 * Handle stop button click
508 * Sends a request to the server to stop the current operation
509 */
510 private async _handleStopClick(): Promise<void> {
511 try {
512 const response = await fetch("cancel", {
513 method: "POST",
514 headers: {
515 "Content-Type": "application/json",
516 },
517 body: JSON.stringify({ reason: "user requested cancellation" }),
518 });
519
520 if (!response.ok) {
521 const errorData = await response.text();
522 throw new Error(
523 `Failed to stop operation: ${response.status} - ${errorData}`,
524 );
525 }
526
527 this.messageStatus = "Stop request sent";
528 } catch (error) {
529 console.error("Error stopping operation:", error);
530 this.messageStatus = "Failed to stop operation";
531 }
532 }
533
Sean McCullough86b56862025-04-18 13:04:03 -0700534 async _sendChat(e: CustomEvent) {
535 console.log("app shell: _sendChat", e);
536 const message = e.detail.message?.trim();
537 if (message == "") {
538 return;
539 }
540 try {
541 // Send the message to the server
542 const response = await fetch("chat", {
543 method: "POST",
544 headers: {
545 "Content-Type": "application/json",
546 },
547 body: JSON.stringify({ message }),
548 });
549
550 if (!response.ok) {
551 const errorData = await response.text();
552 throw new Error(`Server error: ${response.status} - ${errorData}`);
553 }
Sean McCullough86b56862025-04-18 13:04:03 -0700554
Philip Zeyliger73db6052025-04-23 13:09:07 -0700555 // TOOD(philip): If the data manager is getting messages out of order, there's a bug?
Sean McCullough86b56862025-04-18 13:04:03 -0700556 // Reset data manager state to force a full refresh after sending a message
557 // This ensures we get all messages in the correct order
558 // Use private API for now - TODO: add a resetState() method to DataManager
559 (this.dataManager as any).nextFetchIndex = 0;
560 (this.dataManager as any).currentFetchStartIndex = 0;
561
Sean McCullough86b56862025-04-18 13:04:03 -0700562 // // If in diff view, switch to conversation view
563 // if (this.viewMode === "diff") {
564 // await this.toggleViewMode("chat");
565 // }
566
567 // Refresh the timeline data to show the new message
568 await this.dataManager.fetchData();
Sean McCullough86b56862025-04-18 13:04:03 -0700569 } catch (error) {
570 console.error("Error sending chat message:", error);
571 const statusText = document.getElementById("statusText");
572 if (statusText) {
573 statusText.textContent = "Error sending message";
574 }
575 }
576 }
577
Pokey Rule4097e532025-04-24 18:55:28 +0100578 private scrollContainerRef = createRef<HTMLElement>();
579
Sean McCullough86b56862025-04-18 13:04:03 -0700580 render() {
581 return html`
Pokey Rule4097e532025-04-24 18:55:28 +0100582 <div id="top-banner">
Sean McCullough86b56862025-04-18 13:04:03 -0700583 <div class="title-container">
584 <h1 class="banner-title">sketch</h1>
585 <h2 id="chatTitle" class="chat-title">${this.title}</h2>
586 </div>
587
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000588 <!-- Views section with tabs -->
589 <sketch-view-mode-select></sketch-view-mode-select>
590
591 <!-- Container status info -->
Sean McCullough86b56862025-04-18 13:04:03 -0700592 <sketch-container-status
593 .state=${this.containerState}
594 ></sketch-container-status>
595
596 <div class="refresh-control">
Sean McCulloughd3906e22025-04-29 17:32:14 +0000597 <button
598 id="stopButton"
599 class="refresh-button stop-button"
600 @click="${this._handleStopClick}"
601 >
Sean McCullough86b56862025-04-18 13:04:03 -0700602 Stop
603 </button>
604
605 <div class="poll-updates">
606 <input type="checkbox" id="pollToggle" checked />
607 <label for="pollToggle">Poll</label>
608 </div>
609
610 <sketch-network-status
611 message=${this.messageStatus}
612 connection=${this.connectionStatus}
613 error=${this.connectionErrorMessage}
614 ></sketch-network-status>
Philip Zeyliger99a9a022025-04-27 15:15:25 +0000615
616 <sketch-call-status
617 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
618 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
619 ></sketch-call-status>
Sean McCullough86b56862025-04-18 13:04:03 -0700620 </div>
621 </div>
622
Pokey Rule4097e532025-04-24 18:55:28 +0100623 <div id="view-container" ${ref(this.scrollContainerRef)}>
624 <div id="view-container-inner">
625 <div
626 class="chat-view ${this.viewMode === "chat" ? "view-active" : ""}"
627 >
628 <sketch-timeline
629 .messages=${this.messages}
630 .scrollContainer=${this.scrollContainerRef}
631 ></sketch-timeline>
632 </div>
633 <div
634 class="diff-view ${this.viewMode === "diff" ? "view-active" : ""}"
635 >
636 <sketch-diff-view
637 .commitHash=${this.currentCommitHash}
638 ></sketch-diff-view>
639 </div>
640 <div
641 class="chart-view ${this.viewMode === "charts"
642 ? "view-active"
643 : ""}"
644 >
645 <sketch-charts .messages=${this.messages}></sketch-charts>
646 </div>
647 <div
648 class="terminal-view ${this.viewMode === "terminal"
649 ? "view-active"
650 : ""}"
651 >
652 <sketch-terminal></sketch-terminal>
653 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700654 </div>
655 </div>
656
Pokey Rule4097e532025-04-24 18:55:28 +0100657 <div id="chat-input">
658 <sketch-chat-input @send-chat="${this._sendChat}"></sketch-chat-input>
659 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700660 `;
661 }
662
663 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700664 * Lifecycle callback when component is first connected to DOM
665 */
666 firstUpdated(): void {
667 if (this.viewMode !== "chat") {
668 return;
669 }
670
671 // Initial scroll to bottom when component is first rendered
672 setTimeout(
673 () => this.scrollTo({ top: this.scrollHeight, behavior: "smooth" }),
Philip Zeyliger72682df2025-04-23 13:09:46 -0700674 50,
Sean McCullough86b56862025-04-18 13:04:03 -0700675 );
676
Sean McCullough71941bd2025-04-18 13:31:48 -0700677 const pollToggleCheckbox = this.renderRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700678 "#pollToggle",
Sean McCullough71941bd2025-04-18 13:31:48 -0700679 ) as HTMLInputElement;
Sean McCullough86b56862025-04-18 13:04:03 -0700680 pollToggleCheckbox?.addEventListener("change", () => {
681 this.dataManager.setPollingEnabled(pollToggleCheckbox.checked);
682 if (!pollToggleCheckbox.checked) {
683 this.connectionStatus = "disabled";
684 this.messageStatus = "Polling stopped";
685 } else {
686 this.messageStatus = "Polling for updates...";
687 }
688 });
689 }
690}
691
692declare global {
693 interface HTMLElementTagNameMap {
694 "sketch-app-shell": SketchAppShell;
695 }
696}