blob: c2d5e6ab89cb31e216fae07c1a60b54e03513c82 [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";
Philip Zeyliger47b71c92025-04-30 15:43:39 +00004import { AgentMessage, GitCommit, 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";
Philip Zeyliger2c4db092025-04-28 16:57:50 -070016import "./sketch-restart-modal";
Pokey Rule4097e532025-04-24 18:55:28 +010017
18import { createRef, ref } from "lit/directives/ref.js";
Sean McCullough485afc62025-04-28 14:28:39 -070019import { SketchChatInput } from "./sketch-chat-input";
Sean McCullough86b56862025-04-18 13:04:03 -070020
21type ViewMode = "chat" | "diff" | "charts" | "terminal";
22
23@customElement("sketch-app-shell")
24export class SketchAppShell extends LitElement {
25 // Current view mode (chat, diff, charts, terminal)
26 @state()
27 viewMode: "chat" | "diff" | "charts" | "terminal" = "chat";
28
29 // Current commit hash for diff view
30 @state()
31 currentCommitHash: string = "";
32
Philip Zeyliger47b71c92025-04-30 15:43:39 +000033 // Last commit information
34 @state()
35 lastCommit: { hash: string; pushedBranch?: string } | null = null;
36
Sean McCullough86b56862025-04-18 13:04:03 -070037 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
38 // Note that these styles only apply to the scope of this web component's
39 // shadow DOM node, so they won't leak out or collide with CSS declared in
40 // other components or the containing web page (...unless you want it to do that).
41 static styles = css`
Philip Zeyliger47b71c92025-04-30 15:43:39 +000042 /* Last commit display styling */
43 .last-commit {
44 display: flex;
45 align-items: center;
46 padding: 3px 8px;
47 background: #f0f7ff;
48 border: 1px solid #c8e1ff;
49 border-radius: 4px;
50 font-family: monospace;
51 font-size: 12px;
52 color: #0366d6;
53 cursor: pointer;
54 position: relative;
55 margin: 0 10px;
56 white-space: nowrap;
57 overflow: hidden;
58 text-overflow: ellipsis;
59 max-width: 180px;
60 transition: background-color 0.2s ease;
61 }
Autoformattercf570962025-04-30 17:27:39 +000062
Philip Zeyliger47b71c92025-04-30 15:43:39 +000063 .last-commit:hover {
64 background-color: #dbedff;
65 }
Autoformattercf570962025-04-30 17:27:39 +000066
Philip Zeyliger47b71c92025-04-30 15:43:39 +000067 .last-commit::before {
68 content: "Last Commit: ";
69 color: #666;
70 margin-right: 4px;
71 font-family: system-ui, sans-serif;
72 font-size: 11px;
73 }
Autoformattercf570962025-04-30 17:27:39 +000074
Philip Zeyliger47b71c92025-04-30 15:43:39 +000075 .copied-indicator {
76 position: absolute;
77 top: -20px;
78 left: 50%;
79 transform: translateX(-50%);
80 background: rgba(40, 167, 69, 0.9);
81 color: white;
82 padding: 2px 6px;
83 border-radius: 3px;
84 font-size: 10px;
85 font-family: system-ui, sans-serif;
86 animation: fadeInOut 2s ease;
87 pointer-events: none;
88 }
Autoformattercf570962025-04-30 17:27:39 +000089
Philip Zeyliger47b71c92025-04-30 15:43:39 +000090 @keyframes fadeInOut {
Autoformattercf570962025-04-30 17:27:39 +000091 0% {
92 opacity: 0;
93 }
94 20% {
95 opacity: 1;
96 }
97 80% {
98 opacity: 1;
99 }
100 100% {
101 opacity: 0;
102 }
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000103 }
Autoformattercf570962025-04-30 17:27:39 +0000104
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000105 .commit-branch-indicator {
106 color: #28a745;
107 }
Autoformattercf570962025-04-30 17:27:39 +0000108
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000109 .commit-hash-indicator {
110 color: #0366d6;
111 }
Sean McCullough86b56862025-04-18 13:04:03 -0700112 :host {
113 display: block;
Sean McCullough71941bd2025-04-18 13:31:48 -0700114 font-family:
115 system-ui,
116 -apple-system,
117 BlinkMacSystemFont,
118 "Segoe UI",
119 Roboto,
120 sans-serif;
Sean McCullough86b56862025-04-18 13:04:03 -0700121 color: #333;
122 line-height: 1.4;
Pokey Rule4097e532025-04-24 18:55:28 +0100123 height: 100vh;
Sean McCullough86b56862025-04-18 13:04:03 -0700124 width: 100%;
125 position: relative;
126 overflow-x: hidden;
Pokey Rule4097e532025-04-24 18:55:28 +0100127 display: flex;
128 flex-direction: column;
Sean McCullough86b56862025-04-18 13:04:03 -0700129 }
130
131 /* Top banner with combined elements */
Pokey Rule4097e532025-04-24 18:55:28 +0100132 #top-banner {
Sean McCullough86b56862025-04-18 13:04:03 -0700133 display: flex;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000134 align-self: stretch;
Sean McCullough86b56862025-04-18 13:04:03 -0700135 justify-content: space-between;
136 align-items: center;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000137 padding: 0 20px;
Sean McCullough86b56862025-04-18 13:04:03 -0700138 margin-bottom: 0;
139 border-bottom: 1px solid #eee;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000140 gap: 20px;
Sean McCullough86b56862025-04-18 13:04:03 -0700141 background: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700142 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000143 width: 100%;
144 height: 48px;
145 padding-right: 30px; /* Extra padding on the right to prevent elements from hitting the edge */
Sean McCullough86b56862025-04-18 13:04:03 -0700146 }
147
Pokey Rule4097e532025-04-24 18:55:28 +0100148 /* View mode container styles - mirroring timeline.css structure */
149 #view-container {
150 align-self: stretch;
151 overflow-y: auto;
152 flex: 1;
153 }
154
155 #view-container-inner {
156 max-width: 1200px;
157 margin: 0 auto;
158 position: relative;
159 padding-bottom: 10px;
160 padding-top: 10px;
161 }
162
163 #chat-input {
164 align-self: flex-end;
165 width: 100%;
166 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
167 }
168
Sean McCullough86b56862025-04-18 13:04:03 -0700169 .banner-title {
170 font-size: 18px;
171 font-weight: 600;
172 margin: 0;
173 min-width: 6em;
174 white-space: nowrap;
175 overflow: hidden;
176 text-overflow: ellipsis;
177 }
178
179 .chat-title {
180 margin: 0;
181 padding: 0;
182 color: rgba(82, 82, 82, 0.85);
Josh Bleecher Snydereb5166a2025-04-30 17:04:20 +0000183 font-size: 14px;
Sean McCullough86b56862025-04-18 13:04:03 -0700184 font-weight: normal;
185 font-style: italic;
186 white-space: nowrap;
187 overflow: hidden;
188 text-overflow: ellipsis;
189 }
190
Sean McCullough86b56862025-04-18 13:04:03 -0700191 /* Allow the container to expand to full width in diff mode */
Pokey Rule46fff972025-04-25 14:57:44 +0100192 #view-container-inner.diff-active {
Sean McCullough86b56862025-04-18 13:04:03 -0700193 max-width: 100%;
Pokey Rule46fff972025-04-25 14:57:44 +0100194 width: 100%;
Sean McCullough86b56862025-04-18 13:04:03 -0700195 }
196
197 /* Individual view styles */
198 .chat-view,
199 .diff-view,
200 .chart-view,
201 .terminal-view {
202 display: none; /* Hidden by default */
203 width: 100%;
204 }
205
206 /* Active view styles - these will be applied via JavaScript */
207 .view-active {
208 display: flex;
209 flex-direction: column;
210 }
211
212 .title-container {
213 display: flex;
214 flex-direction: column;
215 white-space: nowrap;
216 overflow: hidden;
217 text-overflow: ellipsis;
Josh Bleecher Snydereb5166a2025-04-30 17:04:20 +0000218 max-width: 30%;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000219 padding: 6px 0;
Sean McCullough86b56862025-04-18 13:04:03 -0700220 }
221
222 .refresh-control {
223 display: flex;
224 align-items: center;
225 margin-bottom: 0;
226 flex-wrap: nowrap;
227 white-space: nowrap;
228 flex-shrink: 0;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000229 gap: 15px;
230 padding-left: 15px;
231 margin-right: 50px;
Sean McCullough86b56862025-04-18 13:04:03 -0700232 }
233
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000234 .restart-button,
235 .stop-button {
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700236 background: #2196f3;
237 color: white;
238 border: none;
239 padding: 4px 10px;
240 border-radius: 4px;
241 cursor: pointer;
242 font-size: 12px;
243 margin-right: 5px;
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000244 display: flex;
245 align-items: center;
246 gap: 6px;
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700247 }
248
249 .restart-button:hover {
250 background-color: #0b7dda;
251 }
252
253 .restart-button:disabled {
254 background-color: #ccc;
255 cursor: not-allowed;
256 opacity: 0.6;
257 }
258
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000259 .stop-button {
260 background: #dc3545;
Sean McCullough86b56862025-04-18 13:04:03 -0700261 color: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700262 }
263
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000264 .stop-button:hover:not(:disabled) {
265 background-color: #c82333;
Sean McCullough86b56862025-04-18 13:04:03 -0700266 }
267
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000268 .stop-button:disabled {
269 background-color: #e9a8ad;
270 cursor: not-allowed;
271 opacity: 0.7;
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000272 }
273
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000274 .stop-button:disabled:hover {
275 background-color: #e9a8ad;
276 }
277
278 .button-icon {
279 width: 16px;
280 height: 16px;
281 }
282
283 @media (max-width: 1400px) {
284 .button-text {
285 display: none;
286 }
287
288 .restart-button,
289 .stop-button {
290 padding: 6px;
291 }
292 }
293
294 /* Removed poll-updates class */
295
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000296 .notifications-toggle {
Sean McCullough86b56862025-04-18 13:04:03 -0700297 display: flex;
298 align-items: center;
Sean McCullough86b56862025-04-18 13:04:03 -0700299 font-size: 12px;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000300 margin-right: 10px;
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000301 cursor: pointer;
302 }
303
304 .bell-icon {
305 width: 20px;
306 height: 20px;
307 position: relative;
308 display: inline-flex;
309 align-items: center;
310 justify-content: center;
311 }
312
313 .bell-disabled::before {
314 content: "";
315 position: absolute;
316 width: 2px;
317 height: 24px;
318 background-color: #dc3545;
319 transform: rotate(45deg);
320 transform-origin: center center;
Sean McCullough86b56862025-04-18 13:04:03 -0700321 }
322 `;
323
324 // Header bar: Network connection status details
325 @property()
326 connectionStatus: ConnectionStatus = "disconnected";
Autoformattercf570962025-04-30 17:27:39 +0000327
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000328 // Track if the last commit info has been copied
329 @state()
330 lastCommitCopied: boolean = false;
Sean McCullough86b56862025-04-18 13:04:03 -0700331
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000332 // Track notification preferences
333 @state()
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000334 notificationsEnabled: boolean = false;
335
336 // Track if the window is focused to control notifications
337 @state()
338 private _windowFocused: boolean = document.hasFocus();
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000339
Sean McCullough86b56862025-04-18 13:04:03 -0700340 @property()
341 connectionErrorMessage: string = "";
342
Sean McCullough86b56862025-04-18 13:04:03 -0700343 // Chat messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100344 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700345 messages: AgentMessage[] = [];
Sean McCullough86b56862025-04-18 13:04:03 -0700346
347 @property()
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000348 set title(value: string) {
349 const oldValue = this._title;
350 this._title = value;
351 this.requestUpdate("title", oldValue);
352 // Update document title when title property changes
353 this.updateDocumentTitle();
354 }
355
356 get title(): string {
357 return this._title;
358 }
359
360 private _title: string = "";
Sean McCullough86b56862025-04-18 13:04:03 -0700361
362 private dataManager = new DataManager();
363
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100364 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700365 containerState: State = {
366 title: "",
367 os: "",
368 message_count: 0,
369 hostname: "",
370 working_dir: "",
371 initial_commit: "",
Philip Zeyliger99a9a022025-04-27 15:15:25 +0000372 outstanding_llm_calls: 0,
373 outstanding_tool_calls: [],
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000374 session_id: "",
375 ssh_available: false,
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700376 ssh_error: "",
377 in_container: false,
378 first_message_index: 0,
Sean McCulloughd9f13372025-04-21 15:08:49 -0700379 };
Sean McCullough86b56862025-04-18 13:04:03 -0700380
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700381 @state()
382 private restartModalOpen = false;
383
Sean McCullough86b56862025-04-18 13:04:03 -0700384 // Mutation observer to detect when new messages are added
385 private mutationObserver: MutationObserver | null = null;
386
387 constructor() {
388 super();
389
390 // Binding methods to this
391 this._handleViewModeSelect = this._handleViewModeSelect.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -0700392 this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
Sean McCullough485afc62025-04-28 14:28:39 -0700393 this._handleMutlipleChoiceSelected =
394 this._handleMutlipleChoiceSelected.bind(this);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000395 this._handleStopClick = this._handleStopClick.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000396 this._handleNotificationsToggle =
397 this._handleNotificationsToggle.bind(this);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000398 this._handleWindowFocus = this._handleWindowFocus.bind(this);
399 this._handleWindowBlur = this._handleWindowBlur.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000400
401 // Load notification preference from localStorage
402 try {
403 const savedPref = localStorage.getItem("sketch-notifications-enabled");
404 if (savedPref !== null) {
405 this.notificationsEnabled = savedPref === "true";
406 }
407 } catch (error) {
408 console.error("Error loading notification preference:", error);
409 }
Sean McCullough86b56862025-04-18 13:04:03 -0700410 }
411
412 // See https://lit.dev/docs/components/lifecycle/
413 connectedCallback() {
414 super.connectedCallback();
415
416 // Initialize client-side nav history.
417 const url = new URL(window.location.href);
418 const mode = url.searchParams.get("view") || "chat";
419 window.history.replaceState({ mode }, "", url.toString());
420
421 this.toggleViewMode(mode as ViewMode, false);
422 // Add popstate event listener to handle browser back/forward navigation
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100423 window.addEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700424
425 // Add event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100426 window.addEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100427 window.addEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700428
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000429 // Add window focus/blur listeners for controlling notifications
430 window.addEventListener("focus", this._handleWindowFocus);
431 window.addEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700432 window.addEventListener(
433 "multiple-choice-selected",
434 this._handleMutlipleChoiceSelected,
435 );
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000436
Sean McCullough86b56862025-04-18 13:04:03 -0700437 // register event listeners
438 this.dataManager.addEventListener(
439 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700440 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700441 );
442 this.dataManager.addEventListener(
443 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700444 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700445 );
446
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000447 // Set initial document title
448 this.updateDocumentTitle();
449
Sean McCullough86b56862025-04-18 13:04:03 -0700450 // Initialize the data manager
451 this.dataManager.initialize();
Autoformattercf570962025-04-30 17:27:39 +0000452
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000453 // Process existing messages for commit info
454 if (this.messages && this.messages.length > 0) {
455 this.updateLastCommitInfo(this.messages);
456 }
Sean McCullough86b56862025-04-18 13:04:03 -0700457 }
458
459 // See https://lit.dev/docs/components/lifecycle/
460 disconnectedCallback() {
461 super.disconnectedCallback();
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100462 window.removeEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700463
464 // Remove event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100465 window.removeEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100466 window.removeEventListener("show-commit-diff", this._handleShowCommitDiff);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000467 window.removeEventListener("focus", this._handleWindowFocus);
468 window.removeEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700469 window.removeEventListener(
470 "multiple-choice-selected",
471 this._handleMutlipleChoiceSelected,
472 );
Sean McCullough86b56862025-04-18 13:04:03 -0700473
474 // unregister data manager event listeners
475 this.dataManager.removeEventListener(
476 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700477 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700478 );
479 this.dataManager.removeEventListener(
480 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700481 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700482 );
483
484 // Disconnect mutation observer if it exists
485 if (this.mutationObserver) {
Sean McCullough86b56862025-04-18 13:04:03 -0700486 this.mutationObserver.disconnect();
487 this.mutationObserver = null;
488 }
489 }
490
Sean McCullough71941bd2025-04-18 13:31:48 -0700491 updateUrlForViewMode(mode: "chat" | "diff" | "charts" | "terminal"): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700492 // Get the current URL without search parameters
493 const url = new URL(window.location.href);
494
495 // Clear existing parameters
496 url.search = "";
497
498 // Only add view parameter if not in default chat view
499 if (mode !== "chat") {
500 url.searchParams.set("view", mode);
Sean McCullough71941bd2025-04-18 13:31:48 -0700501 const diffView = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700502 ".diff-view",
Sean McCullough71941bd2025-04-18 13:31:48 -0700503 ) as SketchDiffView;
Sean McCullough86b56862025-04-18 13:04:03 -0700504
505 // If in diff view and there's a commit hash, include that too
506 if (mode === "diff" && diffView.commitHash) {
507 url.searchParams.set("commit", diffView.commitHash);
508 }
509 }
510
511 // Update the browser history without reloading the page
512 window.history.pushState({ mode }, "", url.toString());
513 }
514
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100515 private _handlePopState(event: PopStateEvent) {
Sean McCullough86b56862025-04-18 13:04:03 -0700516 if (event.state && event.state.mode) {
517 this.toggleViewMode(event.state.mode, false);
518 } else {
519 this.toggleViewMode("chat", false);
520 }
521 }
522
523 /**
524 * Handle view mode selection event
525 */
526 private _handleViewModeSelect(event: CustomEvent) {
527 const mode = event.detail.mode as "chat" | "diff" | "charts" | "terminal";
528 this.toggleViewMode(mode, true);
529 }
530
531 /**
532 * Handle show commit diff event
533 */
534 private _handleShowCommitDiff(event: CustomEvent) {
535 const { commitHash } = event.detail;
536 if (commitHash) {
537 this.showCommitDiff(commitHash);
538 }
539 }
540
Sean McCullough485afc62025-04-28 14:28:39 -0700541 private _handleMultipleChoice(event: CustomEvent) {
542 window.console.log("_handleMultipleChoice", event);
543 this._sendChat;
544 }
Sean McCullough86b56862025-04-18 13:04:03 -0700545 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700546 * Listen for commit diff event
547 * @param commitHash The commit hash to show diff for
548 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100549 private showCommitDiff(commitHash: string): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700550 // Store the commit hash
551 this.currentCommitHash = commitHash;
552
553 // Switch to diff view
Sean McCullough71941bd2025-04-18 13:31:48 -0700554 this.toggleViewMode("diff", true);
Sean McCullough86b56862025-04-18 13:04:03 -0700555
556 // Wait for DOM update to complete
557 this.updateComplete.then(() => {
558 // Get the diff view component
559 const diffView = this.shadowRoot?.querySelector("sketch-diff-view");
560 if (diffView) {
561 // Call the showCommitDiff method
562 (diffView as any).showCommitDiff(commitHash);
563 }
564 });
565 }
566
567 /**
568 * Toggle between different view modes: chat, diff, charts, terminal
569 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100570 private toggleViewMode(mode: ViewMode, updateHistory: boolean): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700571 // Don't do anything if the mode is already active
572 if (this.viewMode === mode) return;
573
574 // Update the view mode
575 this.viewMode = mode;
576
577 if (updateHistory) {
578 // Update URL with the current view mode
579 this.updateUrlForViewMode(mode);
580 }
581
582 // Wait for DOM update to complete
583 this.updateComplete.then(() => {
584 // Update active view
Pokey Rule46fff972025-04-25 14:57:44 +0100585 const viewContainerInner = this.shadowRoot?.querySelector(
586 "#view-container-inner",
587 );
Sean McCullough86b56862025-04-18 13:04:03 -0700588 const chatView = this.shadowRoot?.querySelector(".chat-view");
589 const diffView = this.shadowRoot?.querySelector(".diff-view");
590 const chartView = this.shadowRoot?.querySelector(".chart-view");
591 const terminalView = this.shadowRoot?.querySelector(".terminal-view");
592
593 // Remove active class from all views
594 chatView?.classList.remove("view-active");
595 diffView?.classList.remove("view-active");
596 chartView?.classList.remove("view-active");
597 terminalView?.classList.remove("view-active");
598
599 // Add/remove diff-active class on view container
600 if (mode === "diff") {
Pokey Rule46fff972025-04-25 14:57:44 +0100601 viewContainerInner?.classList.add("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700602 } else {
Pokey Rule46fff972025-04-25 14:57:44 +0100603 viewContainerInner?.classList.remove("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700604 }
605
606 // Add active class to the selected view
607 switch (mode) {
608 case "chat":
609 chatView?.classList.add("view-active");
610 break;
611 case "diff":
612 diffView?.classList.add("view-active");
613 // Load diff content if we have a diff view
614 const diffViewComp =
615 this.shadowRoot?.querySelector("sketch-diff-view");
616 if (diffViewComp && this.currentCommitHash) {
617 (diffViewComp as any).showCommitDiff(this.currentCommitHash);
618 } else if (diffViewComp) {
619 (diffViewComp as any).loadDiffContent();
620 }
621 break;
622 case "charts":
623 chartView?.classList.add("view-active");
624 break;
625 case "terminal":
626 terminalView?.classList.add("view-active");
627 break;
628 }
629
630 // Update view mode buttons
631 const viewModeSelect = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700632 "sketch-view-mode-select",
Sean McCullough86b56862025-04-18 13:04:03 -0700633 );
634 if (viewModeSelect) {
635 const event = new CustomEvent("update-active-mode", {
636 detail: { mode },
637 bubbles: true,
638 composed: true,
639 });
640 viewModeSelect.dispatchEvent(event);
641 }
642
643 // FIXME: This is a hack to get vega chart in sketch-charts.ts to work properly
644 // When the chart is in the background, its container has a width of 0, so vega
645 // renders width 0 and only changes that width on a resize event.
646 // See https://github.com/vega/react-vega/issues/85#issuecomment-1826421132
647 window.dispatchEvent(new Event("resize"));
648 });
649 }
650
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000651 /**
652 * Updates the document title based on current title and connection status
653 */
654 private updateDocumentTitle(): void {
655 let docTitle = `sk: ${this.title || "untitled"}`;
656
657 // Add red circle emoji if disconnected
658 if (this.connectionStatus === "disconnected") {
659 docTitle += " 🔴";
660 }
661
662 document.title = docTitle;
663 }
664
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000665 // Check and request notification permission if needed
666 private async checkNotificationPermission(): Promise<boolean> {
667 // Check if the Notification API is supported
668 if (!("Notification" in window)) {
669 console.log("This browser does not support notifications");
670 return false;
671 }
672
673 // Check if permission is already granted
674 if (Notification.permission === "granted") {
675 return true;
676 }
677
678 // If permission is not denied, request it
679 if (Notification.permission !== "denied") {
680 const permission = await Notification.requestPermission();
681 return permission === "granted";
682 }
683
684 return false;
685 }
686
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000687 // Handle notifications toggle click
688 private _handleNotificationsToggle(): void {
689 this.notificationsEnabled = !this.notificationsEnabled;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000690
691 // If enabling notifications, check permissions
692 if (this.notificationsEnabled) {
693 this.checkNotificationPermission();
694 }
695
696 // Save preference to localStorage
697 try {
698 localStorage.setItem(
699 "sketch-notifications-enabled",
700 String(this.notificationsEnabled),
701 );
702 } catch (error) {
703 console.error("Error saving notification preference:", error);
704 }
705 }
706
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000707 // Handle window focus event
708 private _handleWindowFocus(): void {
709 this._windowFocused = true;
710 }
711
712 // Handle window blur event
713 private _handleWindowBlur(): void {
714 this._windowFocused = false;
715 }
716
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000717 // Show notification for message with EndOfTurn=true
718 private async showEndOfTurnNotification(
719 message: AgentMessage,
720 ): Promise<void> {
721 // Don't show notifications if they're disabled
722 if (!this.notificationsEnabled) return;
723
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000724 // Don't show notifications if the window is focused
725 if (this._windowFocused) return;
726
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000727 // Check if we have permission to show notifications
728 const hasPermission = await this.checkNotificationPermission();
729 if (!hasPermission) return;
730
Philip Zeyliger32011332025-04-30 20:59:40 +0000731 // Only show notifications for agent messages with end_of_turn=true and no parent_conversation_id
732 if (
733 message.type !== "agent" ||
734 !message.end_of_turn ||
735 message.parent_conversation_id
736 )
737 return;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000738
739 // Create a title that includes the sketch title
740 const notificationTitle = `Sketch: ${this.title || "untitled"}`;
741
742 // Extract the beginning of the message content (first 100 chars)
743 const messagePreview = message.content
744 ? message.content.substring(0, 100) +
745 (message.content.length > 100 ? "..." : "")
746 : "Agent has completed its turn";
747
748 // Create and show the notification
749 try {
750 new Notification(notificationTitle, {
751 body: messagePreview,
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000752 icon: "https://sketch.dev/favicon.ico", // Use sketch.dev favicon for notification
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000753 });
754 } catch (error) {
755 console.error("Error showing notification:", error);
756 }
757 }
758
Sean McCullough86b56862025-04-18 13:04:03 -0700759 private handleDataChanged(eventData: {
760 state: State;
Sean McCulloughd9f13372025-04-21 15:08:49 -0700761 newMessages: AgentMessage[];
Sean McCullough86b56862025-04-18 13:04:03 -0700762 }): void {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000763 const { state, newMessages } = eventData;
Sean McCullough86b56862025-04-18 13:04:03 -0700764
765 // Update state if we received it
766 if (state) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000767 // Ensure we're using the latest call status to prevent indicators from being stuck
Autoformatterf830c9d2025-04-30 18:16:01 +0000768 if (
769 state.outstanding_llm_calls === 0 &&
770 state.outstanding_tool_calls.length === 0
771 ) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000772 // Force reset containerState calls when nothing is reported as in progress
773 state.outstanding_llm_calls = 0;
774 state.outstanding_tool_calls = [];
775 }
Autoformatterf830c9d2025-04-30 18:16:01 +0000776
Sean McCullough86b56862025-04-18 13:04:03 -0700777 this.containerState = state;
778 this.title = state.title;
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000779
780 // Update document title when sketch title changes
781 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700782 }
783
Sean McCullough86b56862025-04-18 13:04:03 -0700784 // Update messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100785 this.messages = aggregateAgentMessages(this.messages, newMessages);
Autoformattercf570962025-04-30 17:27:39 +0000786
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000787 // Process new messages to find commit messages
788 this.updateLastCommitInfo(newMessages);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000789
790 // Check for agent messages with end_of_turn=true and show notifications
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000791 if (newMessages && newMessages.length > 0) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000792 for (const message of newMessages) {
Philip Zeyliger32011332025-04-30 20:59:40 +0000793 if (
794 message.type === "agent" &&
795 message.end_of_turn &&
796 !message.parent_conversation_id
797 ) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000798 this.showEndOfTurnNotification(message);
799 break; // Only show one notification per batch of messages
800 }
801 }
802 }
Sean McCullough86b56862025-04-18 13:04:03 -0700803 }
804
805 private handleConnectionStatusChanged(
806 status: ConnectionStatus,
Philip Zeyliger72682df2025-04-23 13:09:46 -0700807 errorMessage?: string,
Sean McCullough86b56862025-04-18 13:04:03 -0700808 ): void {
809 this.connectionStatus = status;
810 this.connectionErrorMessage = errorMessage || "";
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000811
812 // Update document title when connection status changes
813 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700814 }
815
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000816 // Update last commit information when new messages arrive
817 private updateLastCommitInfo(newMessages: AgentMessage[]): void {
818 if (!newMessages || newMessages.length === 0) return;
Autoformattercf570962025-04-30 17:27:39 +0000819
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000820 // Process messages in chronological order (latest last)
821 for (const message of newMessages) {
Autoformattercf570962025-04-30 17:27:39 +0000822 if (
823 message.type === "commit" &&
824 message.commits &&
825 message.commits.length > 0
826 ) {
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000827 // Get the first commit from the list
828 const commit = message.commits[0];
829 if (commit) {
830 this.lastCommit = {
831 hash: commit.hash,
Autoformattercf570962025-04-30 17:27:39 +0000832 pushedBranch: commit.pushed_branch,
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000833 };
834 this.lastCommitCopied = false;
835 }
836 }
837 }
838 }
839
840 // Copy commit info to clipboard
841 private copyCommitInfo(event: MouseEvent): void {
842 event.preventDefault();
843 event.stopPropagation();
Autoformattercf570962025-04-30 17:27:39 +0000844
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000845 if (!this.lastCommit) return;
Autoformattercf570962025-04-30 17:27:39 +0000846
847 const textToCopy =
848 this.lastCommit.pushedBranch || this.lastCommit.hash.substring(0, 8);
849
850 navigator.clipboard
851 .writeText(textToCopy)
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000852 .then(() => {
853 this.lastCommitCopied = true;
854 // Reset the copied state after 2 seconds
855 setTimeout(() => {
856 this.lastCommitCopied = false;
857 }, 2000);
858 })
Autoformattercf570962025-04-30 17:27:39 +0000859 .catch((err) => {
860 console.error("Failed to copy commit info:", err);
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000861 });
862 }
Autoformattercf570962025-04-30 17:27:39 +0000863
Sean McCulloughd3906e22025-04-29 17:32:14 +0000864 private async _handleStopClick(): Promise<void> {
865 try {
866 const response = await fetch("cancel", {
867 method: "POST",
868 headers: {
869 "Content-Type": "application/json",
870 },
871 body: JSON.stringify({ reason: "user requested cancellation" }),
872 });
873
874 if (!response.ok) {
875 const errorData = await response.text();
876 throw new Error(
877 `Failed to stop operation: ${response.status} - ${errorData}`,
878 );
879 }
880
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000881 // Stop request sent
Sean McCulloughd3906e22025-04-29 17:32:14 +0000882 } catch (error) {
883 console.error("Error stopping operation:", error);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000884 }
885 }
886
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700887 openRestartModal() {
888 this.restartModalOpen = true;
889 }
890
891 handleRestartModalClose() {
892 this.restartModalOpen = false;
893 }
894
Sean McCullough485afc62025-04-28 14:28:39 -0700895 async _handleMutlipleChoiceSelected(e: CustomEvent) {
896 const chatInput = this.shadowRoot?.querySelector(
897 "sketch-chat-input",
898 ) as SketchChatInput;
899 if (chatInput) {
900 chatInput.content = e.detail.responseText;
901 chatInput.focus();
902 }
903 }
904
Sean McCullough86b56862025-04-18 13:04:03 -0700905 async _sendChat(e: CustomEvent) {
906 console.log("app shell: _sendChat", e);
Sean McCullough485afc62025-04-28 14:28:39 -0700907 e.preventDefault();
908 e.stopPropagation();
Sean McCullough86b56862025-04-18 13:04:03 -0700909 const message = e.detail.message?.trim();
910 if (message == "") {
911 return;
912 }
913 try {
914 // Send the message to the server
915 const response = await fetch("chat", {
916 method: "POST",
917 headers: {
918 "Content-Type": "application/json",
919 },
920 body: JSON.stringify({ message }),
921 });
922
923 if (!response.ok) {
924 const errorData = await response.text();
925 throw new Error(`Server error: ${response.status} - ${errorData}`);
926 }
Sean McCullough86b56862025-04-18 13:04:03 -0700927 } catch (error) {
928 console.error("Error sending chat message:", error);
929 const statusText = document.getElementById("statusText");
930 if (statusText) {
931 statusText.textContent = "Error sending message";
932 }
933 }
934 }
935
Pokey Rule4097e532025-04-24 18:55:28 +0100936 private scrollContainerRef = createRef<HTMLElement>();
937
Sean McCullough86b56862025-04-18 13:04:03 -0700938 render() {
939 return html`
Pokey Rule4097e532025-04-24 18:55:28 +0100940 <div id="top-banner">
Sean McCullough86b56862025-04-18 13:04:03 -0700941 <div class="title-container">
942 <h1 class="banner-title">sketch</h1>
943 <h2 id="chatTitle" class="chat-title">${this.title}</h2>
944 </div>
945
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000946 <!-- Container status info moved above tabs -->
Sean McCullough86b56862025-04-18 13:04:03 -0700947 <sketch-container-status
948 .state=${this.containerState}
949 ></sketch-container-status>
Autoformattercf570962025-04-30 17:27:39 +0000950
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000951 <!-- Views section with tabs - repositioned -->
952 <sketch-view-mode-select></sketch-view-mode-select>
953
Autoformattercf570962025-04-30 17:27:39 +0000954 ${this.lastCommit
955 ? html`
956 <div
957 class="last-commit"
958 @click=${(e: MouseEvent) => this.copyCommitInfo(e)}
959 title="Click to copy"
960 >
961 ${this.lastCommitCopied
962 ? html`<span class="copied-indicator">Copied!</span>`
963 : ""}
964 ${this.lastCommit.pushedBranch
965 ? html`<span class="commit-branch-indicator"
966 >${this.lastCommit.pushedBranch}</span
967 >`
968 : html`<span class="commit-hash-indicator"
969 >${this.lastCommit.hash.substring(0, 8)}</span
970 >`}
971 </div>
972 `
973 : ""}
Sean McCullough86b56862025-04-18 13:04:03 -0700974
975 <div class="refresh-control">
Sean McCulloughd3906e22025-04-29 17:32:14 +0000976 <button
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700977 id="restartButton"
978 class="restart-button"
979 ?disabled=${this.containerState.message_count === 0}
980 @click=${this.openRestartModal}
Sean McCulloughd3906e22025-04-29 17:32:14 +0000981 >
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000982 <svg
983 class="button-icon"
984 xmlns="http://www.w3.org/2000/svg"
985 viewBox="0 0 24 24"
986 fill="none"
987 stroke="currentColor"
988 stroke-width="2"
989 stroke-linecap="round"
990 stroke-linejoin="round"
991 >
992 <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
993 <path d="M3 3v5h5" />
994 </svg>
995 <span class="button-text">Restart</span>
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700996 </button>
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000997 <button
998 id="stopButton"
999 class="stop-button"
1000 ?disabled=${(this.containerState?.outstanding_llm_calls || 0) ===
1001 0 &&
1002 (this.containerState?.outstanding_tool_calls || []).length === 0}
1003 >
1004 <svg
1005 class="button-icon"
1006 xmlns="http://www.w3.org/2000/svg"
1007 viewBox="0 0 24 24"
1008 fill="none"
1009 stroke="currentColor"
1010 stroke-width="2"
1011 stroke-linecap="round"
1012 stroke-linejoin="round"
1013 >
1014 <rect x="6" y="6" width="12" height="12" />
1015 </svg>
1016 <span class="button-text">Stop</span>
Sean McCullough86b56862025-04-18 13:04:03 -07001017 </button>
1018
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +00001019 <div
1020 class="notifications-toggle"
1021 @click=${this._handleNotificationsToggle}
1022 title="${this.notificationsEnabled
1023 ? "Disable"
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001024 : "Enable"} notifications when the agent completes its turn"
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +00001025 >
1026 <div
1027 class="bell-icon ${!this.notificationsEnabled
1028 ? "bell-disabled"
1029 : ""}"
1030 >
1031 <!-- Bell SVG icon -->
1032 <svg
1033 xmlns="http://www.w3.org/2000/svg"
1034 width="16"
1035 height="16"
1036 fill="currentColor"
1037 viewBox="0 0 16 16"
1038 >
1039 <path
1040 d="M8 16a2 2 0 0 0 2-2H6a2 2 0 0 0 2 2zM8 1.918l-.797.161A4.002 4.002 0 0 0 4 6c0 .628-.134 2.197-.459 3.742-.16.767-.376 1.566-.663 2.258h10.244c-.287-.692-.502-1.49-.663-2.258C12.134 8.197 12 6.628 12 6a4.002 4.002 0 0 0-3.203-3.92L8 1.917zM14.22 12c.223.447.481.801.78 1H1c.299-.199.557-.553.78-1C2.68 10.2 3 6.88 3 6c0-2.42 1.72-4.44 4.005-4.901a1 1 0 1 1 1.99 0A5.002 5.002 0 0 1 13 6c0 .88.32 4.2 1.22 6z"
1041 />
1042 </svg>
1043 </div>
Philip Zeyligerbc6b6292025-04-30 18:00:15 +00001044 </div>
1045
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001046 <sketch-call-status
Sean McCulloughd9d45812025-04-30 16:53:41 -07001047 .agentState=${this.containerState?.agent_state}
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001048 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
1049 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
1050 ></sketch-call-status>
Philip Zeyliger25f6ff12025-05-02 04:24:10 +00001051
1052 <sketch-network-status
1053 connection=${this.connectionStatus}
1054 error=${this.connectionErrorMessage}
1055 ></sketch-network-status>
Sean McCullough86b56862025-04-18 13:04:03 -07001056 </div>
1057 </div>
1058
Pokey Rule4097e532025-04-24 18:55:28 +01001059 <div id="view-container" ${ref(this.scrollContainerRef)}>
1060 <div id="view-container-inner">
1061 <div
1062 class="chat-view ${this.viewMode === "chat" ? "view-active" : ""}"
1063 >
1064 <sketch-timeline
1065 .messages=${this.messages}
1066 .scrollContainer=${this.scrollContainerRef}
1067 ></sketch-timeline>
1068 </div>
1069 <div
1070 class="diff-view ${this.viewMode === "diff" ? "view-active" : ""}"
1071 >
1072 <sketch-diff-view
1073 .commitHash=${this.currentCommitHash}
1074 ></sketch-diff-view>
1075 </div>
1076 <div
1077 class="chart-view ${this.viewMode === "charts"
1078 ? "view-active"
1079 : ""}"
1080 >
1081 <sketch-charts .messages=${this.messages}></sketch-charts>
1082 </div>
1083 <div
1084 class="terminal-view ${this.viewMode === "terminal"
1085 ? "view-active"
1086 : ""}"
1087 >
1088 <sketch-terminal></sketch-terminal>
1089 </div>
Sean McCullough86b56862025-04-18 13:04:03 -07001090 </div>
1091 </div>
1092
Pokey Rule4097e532025-04-24 18:55:28 +01001093 <div id="chat-input">
1094 <sketch-chat-input @send-chat="${this._sendChat}"></sketch-chat-input>
1095 </div>
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001096
1097 <sketch-restart-modal
1098 ?open=${this.restartModalOpen}
1099 @close=${this.handleRestartModalClose}
1100 .containerState=${this.containerState}
1101 .messages=${this.messages}
1102 ></sketch-restart-modal>
Sean McCullough86b56862025-04-18 13:04:03 -07001103 `;
1104 }
1105
1106 /**
Sean McCullough86b56862025-04-18 13:04:03 -07001107 * Lifecycle callback when component is first connected to DOM
1108 */
1109 firstUpdated(): void {
1110 if (this.viewMode !== "chat") {
1111 return;
1112 }
1113
1114 // Initial scroll to bottom when component is first rendered
1115 setTimeout(
1116 () => this.scrollTo({ top: this.scrollHeight, behavior: "smooth" }),
Philip Zeyliger72682df2025-04-23 13:09:46 -07001117 50,
Sean McCullough86b56862025-04-18 13:04:03 -07001118 );
1119
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001120 // Setup stop button
1121 const stopButton = this.renderRoot?.querySelector(
1122 "#stopButton",
1123 ) as HTMLButtonElement;
1124 stopButton?.addEventListener("click", async () => {
1125 try {
Sean McCullough495cb962025-05-01 16:25:53 -07001126 const response = await fetch("cancel", {
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001127 method: "POST",
1128 headers: {
1129 "Content-Type": "application/json",
1130 },
1131 body: JSON.stringify({ reason: "User clicked stop button" }),
1132 });
1133 if (!response.ok) {
1134 console.error("Failed to cancel:", await response.text());
1135 }
1136 } catch (error) {
1137 console.error("Error cancelling operation:", error);
1138 }
1139 });
1140
Philip Zeyliger47b71c92025-04-30 15:43:39 +00001141 // Process any existing messages to find commit information
1142 if (this.messages && this.messages.length > 0) {
1143 this.updateLastCommitInfo(this.messages);
1144 }
Sean McCullough86b56862025-04-18 13:04:03 -07001145 }
1146}
1147
1148declare global {
1149 interface HTMLElementTagNameMap {
1150 "sketch-app-shell": SketchAppShell;
1151 }
1152}