blob: 9682daf4d046d731464af40263e15992b35f6df6 [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: [],
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000221 session_id: "",
222 ssh_available: false,
Sean McCulloughd9f13372025-04-21 15:08:49 -0700223 };
Sean McCullough86b56862025-04-18 13:04:03 -0700224
Sean McCullough86b56862025-04-18 13:04:03 -0700225 // Mutation observer to detect when new messages are added
226 private mutationObserver: MutationObserver | null = null;
227
228 constructor() {
229 super();
230
231 // Binding methods to this
232 this._handleViewModeSelect = this._handleViewModeSelect.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -0700233 this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
234 this._handlePopState = this._handlePopState.bind(this);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000235 this._handleStopClick = this._handleStopClick.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -0700236 }
237
238 // See https://lit.dev/docs/components/lifecycle/
239 connectedCallback() {
240 super.connectedCallback();
241
242 // Initialize client-side nav history.
243 const url = new URL(window.location.href);
244 const mode = url.searchParams.get("view") || "chat";
245 window.history.replaceState({ mode }, "", url.toString());
246
247 this.toggleViewMode(mode as ViewMode, false);
248 // Add popstate event listener to handle browser back/forward navigation
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100249 window.addEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700250
251 // Add event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100252 window.addEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100253 window.addEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700254
255 // register event listeners
256 this.dataManager.addEventListener(
257 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700258 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700259 );
260 this.dataManager.addEventListener(
261 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700262 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700263 );
264
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000265 // Set initial document title
266 this.updateDocumentTitle();
267
Sean McCullough86b56862025-04-18 13:04:03 -0700268 // Initialize the data manager
269 this.dataManager.initialize();
270 }
271
272 // See https://lit.dev/docs/components/lifecycle/
273 disconnectedCallback() {
274 super.disconnectedCallback();
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100275 window.removeEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700276
277 // Remove event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100278 window.removeEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100279 window.removeEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700280
281 // unregister data manager event listeners
282 this.dataManager.removeEventListener(
283 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700284 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700285 );
286 this.dataManager.removeEventListener(
287 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700288 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700289 );
290
291 // Disconnect mutation observer if it exists
292 if (this.mutationObserver) {
Sean McCullough86b56862025-04-18 13:04:03 -0700293 this.mutationObserver.disconnect();
294 this.mutationObserver = null;
295 }
296 }
297
Sean McCullough71941bd2025-04-18 13:31:48 -0700298 updateUrlForViewMode(mode: "chat" | "diff" | "charts" | "terminal"): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700299 // Get the current URL without search parameters
300 const url = new URL(window.location.href);
301
302 // Clear existing parameters
303 url.search = "";
304
305 // Only add view parameter if not in default chat view
306 if (mode !== "chat") {
307 url.searchParams.set("view", mode);
Sean McCullough71941bd2025-04-18 13:31:48 -0700308 const diffView = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700309 ".diff-view",
Sean McCullough71941bd2025-04-18 13:31:48 -0700310 ) as SketchDiffView;
Sean McCullough86b56862025-04-18 13:04:03 -0700311
312 // If in diff view and there's a commit hash, include that too
313 if (mode === "diff" && diffView.commitHash) {
314 url.searchParams.set("commit", diffView.commitHash);
315 }
316 }
317
318 // Update the browser history without reloading the page
319 window.history.pushState({ mode }, "", url.toString());
320 }
321
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100322 private _handlePopState(event: PopStateEvent) {
Sean McCullough86b56862025-04-18 13:04:03 -0700323 if (event.state && event.state.mode) {
324 this.toggleViewMode(event.state.mode, false);
325 } else {
326 this.toggleViewMode("chat", false);
327 }
328 }
329
330 /**
331 * Handle view mode selection event
332 */
333 private _handleViewModeSelect(event: CustomEvent) {
334 const mode = event.detail.mode as "chat" | "diff" | "charts" | "terminal";
335 this.toggleViewMode(mode, true);
336 }
337
338 /**
339 * Handle show commit diff event
340 */
341 private _handleShowCommitDiff(event: CustomEvent) {
342 const { commitHash } = event.detail;
343 if (commitHash) {
344 this.showCommitDiff(commitHash);
345 }
346 }
347
348 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700349 * Listen for commit diff event
350 * @param commitHash The commit hash to show diff for
351 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100352 private showCommitDiff(commitHash: string): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700353 // Store the commit hash
354 this.currentCommitHash = commitHash;
355
356 // Switch to diff view
Sean McCullough71941bd2025-04-18 13:31:48 -0700357 this.toggleViewMode("diff", true);
Sean McCullough86b56862025-04-18 13:04:03 -0700358
359 // Wait for DOM update to complete
360 this.updateComplete.then(() => {
361 // Get the diff view component
362 const diffView = this.shadowRoot?.querySelector("sketch-diff-view");
363 if (diffView) {
364 // Call the showCommitDiff method
365 (diffView as any).showCommitDiff(commitHash);
366 }
367 });
368 }
369
370 /**
371 * Toggle between different view modes: chat, diff, charts, terminal
372 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100373 private toggleViewMode(mode: ViewMode, updateHistory: boolean): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700374 // Don't do anything if the mode is already active
375 if (this.viewMode === mode) return;
376
377 // Update the view mode
378 this.viewMode = mode;
379
380 if (updateHistory) {
381 // Update URL with the current view mode
382 this.updateUrlForViewMode(mode);
383 }
384
385 // Wait for DOM update to complete
386 this.updateComplete.then(() => {
387 // Update active view
Pokey Rule46fff972025-04-25 14:57:44 +0100388 const viewContainerInner = this.shadowRoot?.querySelector(
389 "#view-container-inner",
390 );
Sean McCullough86b56862025-04-18 13:04:03 -0700391 const chatView = this.shadowRoot?.querySelector(".chat-view");
392 const diffView = this.shadowRoot?.querySelector(".diff-view");
393 const chartView = this.shadowRoot?.querySelector(".chart-view");
394 const terminalView = this.shadowRoot?.querySelector(".terminal-view");
395
396 // Remove active class from all views
397 chatView?.classList.remove("view-active");
398 diffView?.classList.remove("view-active");
399 chartView?.classList.remove("view-active");
400 terminalView?.classList.remove("view-active");
401
402 // Add/remove diff-active class on view container
403 if (mode === "diff") {
Pokey Rule46fff972025-04-25 14:57:44 +0100404 viewContainerInner?.classList.add("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700405 } else {
Pokey Rule46fff972025-04-25 14:57:44 +0100406 viewContainerInner?.classList.remove("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700407 }
408
409 // Add active class to the selected view
410 switch (mode) {
411 case "chat":
412 chatView?.classList.add("view-active");
413 break;
414 case "diff":
415 diffView?.classList.add("view-active");
416 // Load diff content if we have a diff view
417 const diffViewComp =
418 this.shadowRoot?.querySelector("sketch-diff-view");
419 if (diffViewComp && this.currentCommitHash) {
420 (diffViewComp as any).showCommitDiff(this.currentCommitHash);
421 } else if (diffViewComp) {
422 (diffViewComp as any).loadDiffContent();
423 }
424 break;
425 case "charts":
426 chartView?.classList.add("view-active");
427 break;
428 case "terminal":
429 terminalView?.classList.add("view-active");
430 break;
431 }
432
433 // Update view mode buttons
434 const viewModeSelect = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700435 "sketch-view-mode-select",
Sean McCullough86b56862025-04-18 13:04:03 -0700436 );
437 if (viewModeSelect) {
438 const event = new CustomEvent("update-active-mode", {
439 detail: { mode },
440 bubbles: true,
441 composed: true,
442 });
443 viewModeSelect.dispatchEvent(event);
444 }
445
446 // FIXME: This is a hack to get vega chart in sketch-charts.ts to work properly
447 // When the chart is in the background, its container has a width of 0, so vega
448 // renders width 0 and only changes that width on a resize event.
449 // See https://github.com/vega/react-vega/issues/85#issuecomment-1826421132
450 window.dispatchEvent(new Event("resize"));
451 });
452 }
453
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000454 /**
455 * Updates the document title based on current title and connection status
456 */
457 private updateDocumentTitle(): void {
458 let docTitle = `sk: ${this.title || "untitled"}`;
459
460 // Add red circle emoji if disconnected
461 if (this.connectionStatus === "disconnected") {
462 docTitle += " 🔴";
463 }
464
465 document.title = docTitle;
466 }
467
Sean McCullough86b56862025-04-18 13:04:03 -0700468 private handleDataChanged(eventData: {
469 state: State;
Sean McCulloughd9f13372025-04-21 15:08:49 -0700470 newMessages: AgentMessage[];
Sean McCullough86b56862025-04-18 13:04:03 -0700471 isFirstFetch?: boolean;
472 }): void {
473 const { state, newMessages, isFirstFetch } = eventData;
474
475 // Check if this is the first data fetch or if there are new messages
476 if (isFirstFetch) {
Sean McCullough86b56862025-04-18 13:04:03 -0700477 this.messageStatus = "Initial messages loaded";
478 } else if (newMessages && newMessages.length > 0) {
Sean McCullough86b56862025-04-18 13:04:03 -0700479 this.messageStatus = "Updated just now";
Sean McCullough86b56862025-04-18 13:04:03 -0700480 } else {
481 this.messageStatus = "No new messages";
482 }
483
484 // Update state if we received it
485 if (state) {
486 this.containerState = state;
487 this.title = state.title;
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000488
489 // Update document title when sketch title changes
490 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700491 }
492
Sean McCullough86b56862025-04-18 13:04:03 -0700493 // Update messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100494 this.messages = aggregateAgentMessages(this.messages, newMessages);
Sean McCullough86b56862025-04-18 13:04:03 -0700495 }
496
497 private handleConnectionStatusChanged(
498 status: ConnectionStatus,
Philip Zeyliger72682df2025-04-23 13:09:46 -0700499 errorMessage?: string,
Sean McCullough86b56862025-04-18 13:04:03 -0700500 ): void {
501 this.connectionStatus = status;
502 this.connectionErrorMessage = errorMessage || "";
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000503
504 // Update document title when connection status changes
505 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700506 }
507
Sean McCulloughd3906e22025-04-29 17:32:14 +0000508 /**
509 * Handle stop button click
510 * Sends a request to the server to stop the current operation
511 */
512 private async _handleStopClick(): Promise<void> {
513 try {
514 const response = await fetch("cancel", {
515 method: "POST",
516 headers: {
517 "Content-Type": "application/json",
518 },
519 body: JSON.stringify({ reason: "user requested cancellation" }),
520 });
521
522 if (!response.ok) {
523 const errorData = await response.text();
524 throw new Error(
525 `Failed to stop operation: ${response.status} - ${errorData}`,
526 );
527 }
528
529 this.messageStatus = "Stop request sent";
530 } catch (error) {
531 console.error("Error stopping operation:", error);
532 this.messageStatus = "Failed to stop operation";
533 }
534 }
535
Sean McCullough86b56862025-04-18 13:04:03 -0700536 async _sendChat(e: CustomEvent) {
537 console.log("app shell: _sendChat", e);
538 const message = e.detail.message?.trim();
539 if (message == "") {
540 return;
541 }
542 try {
543 // Send the message to the server
544 const response = await fetch("chat", {
545 method: "POST",
546 headers: {
547 "Content-Type": "application/json",
548 },
549 body: JSON.stringify({ message }),
550 });
551
552 if (!response.ok) {
553 const errorData = await response.text();
554 throw new Error(`Server error: ${response.status} - ${errorData}`);
555 }
Sean McCullough86b56862025-04-18 13:04:03 -0700556
Philip Zeyliger73db6052025-04-23 13:09:07 -0700557 // TOOD(philip): If the data manager is getting messages out of order, there's a bug?
Sean McCullough86b56862025-04-18 13:04:03 -0700558 // Reset data manager state to force a full refresh after sending a message
559 // This ensures we get all messages in the correct order
560 // Use private API for now - TODO: add a resetState() method to DataManager
561 (this.dataManager as any).nextFetchIndex = 0;
562 (this.dataManager as any).currentFetchStartIndex = 0;
563
Sean McCullough86b56862025-04-18 13:04:03 -0700564 // // If in diff view, switch to conversation view
565 // if (this.viewMode === "diff") {
566 // await this.toggleViewMode("chat");
567 // }
568
569 // Refresh the timeline data to show the new message
570 await this.dataManager.fetchData();
Sean McCullough86b56862025-04-18 13:04:03 -0700571 } catch (error) {
572 console.error("Error sending chat message:", error);
573 const statusText = document.getElementById("statusText");
574 if (statusText) {
575 statusText.textContent = "Error sending message";
576 }
577 }
578 }
579
Pokey Rule4097e532025-04-24 18:55:28 +0100580 private scrollContainerRef = createRef<HTMLElement>();
581
Sean McCullough86b56862025-04-18 13:04:03 -0700582 render() {
583 return html`
Pokey Rule4097e532025-04-24 18:55:28 +0100584 <div id="top-banner">
Sean McCullough86b56862025-04-18 13:04:03 -0700585 <div class="title-container">
586 <h1 class="banner-title">sketch</h1>
587 <h2 id="chatTitle" class="chat-title">${this.title}</h2>
588 </div>
589
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000590 <!-- Views section with tabs -->
591 <sketch-view-mode-select></sketch-view-mode-select>
592
593 <!-- Container status info -->
Sean McCullough86b56862025-04-18 13:04:03 -0700594 <sketch-container-status
595 .state=${this.containerState}
596 ></sketch-container-status>
597
598 <div class="refresh-control">
Sean McCulloughd3906e22025-04-29 17:32:14 +0000599 <button
600 id="stopButton"
601 class="refresh-button stop-button"
602 @click="${this._handleStopClick}"
603 >
Sean McCullough86b56862025-04-18 13:04:03 -0700604 Stop
605 </button>
606
607 <div class="poll-updates">
608 <input type="checkbox" id="pollToggle" checked />
609 <label for="pollToggle">Poll</label>
610 </div>
611
612 <sketch-network-status
613 message=${this.messageStatus}
614 connection=${this.connectionStatus}
615 error=${this.connectionErrorMessage}
616 ></sketch-network-status>
Philip Zeyliger99a9a022025-04-27 15:15:25 +0000617
618 <sketch-call-status
619 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
620 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
621 ></sketch-call-status>
Sean McCullough86b56862025-04-18 13:04:03 -0700622 </div>
623 </div>
624
Pokey Rule4097e532025-04-24 18:55:28 +0100625 <div id="view-container" ${ref(this.scrollContainerRef)}>
626 <div id="view-container-inner">
627 <div
628 class="chat-view ${this.viewMode === "chat" ? "view-active" : ""}"
629 >
630 <sketch-timeline
631 .messages=${this.messages}
632 .scrollContainer=${this.scrollContainerRef}
633 ></sketch-timeline>
634 </div>
635 <div
636 class="diff-view ${this.viewMode === "diff" ? "view-active" : ""}"
637 >
638 <sketch-diff-view
639 .commitHash=${this.currentCommitHash}
640 ></sketch-diff-view>
641 </div>
642 <div
643 class="chart-view ${this.viewMode === "charts"
644 ? "view-active"
645 : ""}"
646 >
647 <sketch-charts .messages=${this.messages}></sketch-charts>
648 </div>
649 <div
650 class="terminal-view ${this.viewMode === "terminal"
651 ? "view-active"
652 : ""}"
653 >
654 <sketch-terminal></sketch-terminal>
655 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700656 </div>
657 </div>
658
Pokey Rule4097e532025-04-24 18:55:28 +0100659 <div id="chat-input">
660 <sketch-chat-input @send-chat="${this._sendChat}"></sketch-chat-input>
661 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700662 `;
663 }
664
665 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700666 * Lifecycle callback when component is first connected to DOM
667 */
668 firstUpdated(): void {
669 if (this.viewMode !== "chat") {
670 return;
671 }
672
673 // Initial scroll to bottom when component is first rendered
674 setTimeout(
675 () => this.scrollTo({ top: this.scrollHeight, behavior: "smooth" }),
Philip Zeyliger72682df2025-04-23 13:09:46 -0700676 50,
Sean McCullough86b56862025-04-18 13:04:03 -0700677 );
678
Sean McCullough71941bd2025-04-18 13:31:48 -0700679 const pollToggleCheckbox = this.renderRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700680 "#pollToggle",
Sean McCullough71941bd2025-04-18 13:31:48 -0700681 ) as HTMLInputElement;
Sean McCullough86b56862025-04-18 13:04:03 -0700682 pollToggleCheckbox?.addEventListener("change", () => {
683 this.dataManager.setPollingEnabled(pollToggleCheckbox.checked);
684 if (!pollToggleCheckbox.checked) {
685 this.connectionStatus = "disabled";
686 this.messageStatus = "Polling stopped";
687 } else {
688 this.messageStatus = "Polling for updates...";
689 }
690 });
691 }
692}
693
694declare global {
695 interface HTMLElementTagNameMap {
696 "sketch-app-shell": SketchAppShell;
697 }
698}