blob: 9b3430d183cbce1f0b045316df3b1ecd7b0ac10b [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";
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +00006
Pokey Rule4097e532025-04-24 18:55:28 +01007import "./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
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +000021type ViewMode = "chat" | "diff" | "terminal";
Sean McCullough86b56862025-04-18 13:04:03 -070022
23@customElement("sketch-app-shell")
24export class SketchAppShell extends LitElement {
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +000025 // Current view mode (chat, diff, terminal)
Sean McCullough86b56862025-04-18 13:04:03 -070026 @state()
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +000027 viewMode: "chat" | "diff" | "terminal" = "chat";
Sean McCullough86b56862025-04-18 13:04:03 -070028
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()
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000035
36 // Reference to the container status element
37 containerStatusElement: any = null;
Philip Zeyliger47b71c92025-04-30 15:43:39 +000038
Sean McCullough86b56862025-04-18 13:04:03 -070039 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
40 // Note that these styles only apply to the scope of this web component's
41 // shadow DOM node, so they won't leak out or collide with CSS declared in
42 // other components or the containing web page (...unless you want it to do that).
43 static styles = css`
Philip Zeyliger47b71c92025-04-30 15:43:39 +000044 .copied-indicator {
45 position: absolute;
46 top: -20px;
47 left: 50%;
48 transform: translateX(-50%);
49 background: rgba(40, 167, 69, 0.9);
50 color: white;
51 padding: 2px 6px;
52 border-radius: 3px;
53 font-size: 10px;
54 font-family: system-ui, sans-serif;
55 animation: fadeInOut 2s ease;
56 pointer-events: none;
57 }
Autoformattercf570962025-04-30 17:27:39 +000058
Philip Zeyliger47b71c92025-04-30 15:43:39 +000059 @keyframes fadeInOut {
Autoformattercf570962025-04-30 17:27:39 +000060 0% {
61 opacity: 0;
62 }
63 20% {
64 opacity: 1;
65 }
66 80% {
67 opacity: 1;
68 }
69 100% {
70 opacity: 0;
71 }
Philip Zeyliger47b71c92025-04-30 15:43:39 +000072 }
Autoformattercf570962025-04-30 17:27:39 +000073
Philip Zeyliger47b71c92025-04-30 15:43:39 +000074 .commit-branch-indicator {
75 color: #28a745;
76 }
Autoformattercf570962025-04-30 17:27:39 +000077
Philip Zeyliger47b71c92025-04-30 15:43:39 +000078 .commit-hash-indicator {
79 color: #0366d6;
80 }
Sean McCullough86b56862025-04-18 13:04:03 -070081 :host {
82 display: block;
Sean McCullough71941bd2025-04-18 13:31:48 -070083 font-family:
84 system-ui,
85 -apple-system,
86 BlinkMacSystemFont,
87 "Segoe UI",
88 Roboto,
89 sans-serif;
Sean McCullough86b56862025-04-18 13:04:03 -070090 color: #333;
91 line-height: 1.4;
Pokey Rule4097e532025-04-24 18:55:28 +010092 height: 100vh;
Sean McCullough86b56862025-04-18 13:04:03 -070093 width: 100%;
94 position: relative;
95 overflow-x: hidden;
Pokey Rule4097e532025-04-24 18:55:28 +010096 display: flex;
97 flex-direction: column;
Sean McCullough86b56862025-04-18 13:04:03 -070098 }
99
100 /* Top banner with combined elements */
Pokey Rule4097e532025-04-24 18:55:28 +0100101 #top-banner {
Sean McCullough86b56862025-04-18 13:04:03 -0700102 display: flex;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000103 align-self: stretch;
Sean McCullough86b56862025-04-18 13:04:03 -0700104 justify-content: space-between;
105 align-items: center;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000106 padding: 0 20px;
Sean McCullough86b56862025-04-18 13:04:03 -0700107 margin-bottom: 0;
108 border-bottom: 1px solid #eee;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000109 gap: 20px;
Sean McCullough86b56862025-04-18 13:04:03 -0700110 background: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700111 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000112 width: 100%;
113 height: 48px;
114 padding-right: 30px; /* Extra padding on the right to prevent elements from hitting the edge */
Sean McCullough86b56862025-04-18 13:04:03 -0700115 }
116
Pokey Rule4097e532025-04-24 18:55:28 +0100117 /* View mode container styles - mirroring timeline.css structure */
118 #view-container {
119 align-self: stretch;
120 overflow-y: auto;
121 flex: 1;
122 }
123
124 #view-container-inner {
125 max-width: 1200px;
126 margin: 0 auto;
127 position: relative;
128 padding-bottom: 10px;
129 padding-top: 10px;
130 }
131
132 #chat-input {
133 align-self: flex-end;
134 width: 100%;
135 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
136 }
137
Sean McCullough86b56862025-04-18 13:04:03 -0700138 .banner-title {
139 font-size: 18px;
140 font-weight: 600;
141 margin: 0;
142 min-width: 6em;
143 white-space: nowrap;
144 overflow: hidden;
145 text-overflow: ellipsis;
146 }
147
148 .chat-title {
149 margin: 0;
150 padding: 0;
151 color: rgba(82, 82, 82, 0.85);
Josh Bleecher Snydereb5166a2025-04-30 17:04:20 +0000152 font-size: 14px;
Sean McCullough86b56862025-04-18 13:04:03 -0700153 font-weight: normal;
154 font-style: italic;
155 white-space: nowrap;
156 overflow: hidden;
157 text-overflow: ellipsis;
158 }
159
Sean McCullough86b56862025-04-18 13:04:03 -0700160 /* Allow the container to expand to full width in diff mode */
Pokey Rule46fff972025-04-25 14:57:44 +0100161 #view-container-inner.diff-active {
Sean McCullough86b56862025-04-18 13:04:03 -0700162 max-width: 100%;
Pokey Rule46fff972025-04-25 14:57:44 +0100163 width: 100%;
Sean McCullough86b56862025-04-18 13:04:03 -0700164 }
165
166 /* Individual view styles */
167 .chat-view,
168 .diff-view,
Sean McCullough86b56862025-04-18 13:04:03 -0700169 .terminal-view {
170 display: none; /* Hidden by default */
171 width: 100%;
172 }
173
174 /* Active view styles - these will be applied via JavaScript */
175 .view-active {
176 display: flex;
177 flex-direction: column;
178 }
179
180 .title-container {
181 display: flex;
182 flex-direction: column;
183 white-space: nowrap;
184 overflow: hidden;
185 text-overflow: ellipsis;
Josh Bleecher Snydereb5166a2025-04-30 17:04:20 +0000186 max-width: 30%;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000187 padding: 6px 0;
Sean McCullough86b56862025-04-18 13:04:03 -0700188 }
189
190 .refresh-control {
191 display: flex;
192 align-items: center;
193 margin-bottom: 0;
194 flex-wrap: nowrap;
195 white-space: nowrap;
196 flex-shrink: 0;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000197 gap: 15px;
198 padding-left: 15px;
199 margin-right: 50px;
Sean McCullough86b56862025-04-18 13:04:03 -0700200 }
201
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000202 .restart-button,
203 .stop-button {
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700204 background: #2196f3;
205 color: white;
206 border: none;
207 padding: 4px 10px;
208 border-radius: 4px;
209 cursor: pointer;
210 font-size: 12px;
211 margin-right: 5px;
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000212 display: flex;
213 align-items: center;
214 gap: 6px;
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700215 }
216
217 .restart-button:hover {
218 background-color: #0b7dda;
219 }
220
221 .restart-button:disabled {
222 background-color: #ccc;
223 cursor: not-allowed;
224 opacity: 0.6;
225 }
226
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000227 .stop-button {
228 background: #dc3545;
Sean McCullough86b56862025-04-18 13:04:03 -0700229 color: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700230 }
231
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000232 .stop-button:hover:not(:disabled) {
233 background-color: #c82333;
Sean McCullough86b56862025-04-18 13:04:03 -0700234 }
235
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000236 .stop-button:disabled {
237 background-color: #e9a8ad;
238 cursor: not-allowed;
239 opacity: 0.7;
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000240 }
241
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000242 .stop-button:disabled:hover {
243 background-color: #e9a8ad;
244 }
245
246 .button-icon {
247 width: 16px;
248 height: 16px;
249 }
250
251 @media (max-width: 1400px) {
252 .button-text {
253 display: none;
254 }
255
256 .restart-button,
257 .stop-button {
258 padding: 6px;
259 }
260 }
261
262 /* Removed poll-updates class */
263
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000264 .notifications-toggle {
Sean McCullough86b56862025-04-18 13:04:03 -0700265 display: flex;
266 align-items: center;
Sean McCullough86b56862025-04-18 13:04:03 -0700267 font-size: 12px;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000268 margin-right: 10px;
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000269 cursor: pointer;
270 }
271
272 .bell-icon {
273 width: 20px;
274 height: 20px;
275 position: relative;
276 display: inline-flex;
277 align-items: center;
278 justify-content: center;
279 }
280
281 .bell-disabled::before {
282 content: "";
283 position: absolute;
284 width: 2px;
285 height: 24px;
286 background-color: #dc3545;
287 transform: rotate(45deg);
288 transform-origin: center center;
Sean McCullough86b56862025-04-18 13:04:03 -0700289 }
290 `;
291
292 // Header bar: Network connection status details
293 @property()
294 connectionStatus: ConnectionStatus = "disconnected";
Autoformattercf570962025-04-30 17:27:39 +0000295
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000296 // Track if the last commit info has been copied
297 @state()
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000298 // lastCommitCopied moved to sketch-container-status
Sean McCullough86b56862025-04-18 13:04:03 -0700299
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000300 // Track notification preferences
301 @state()
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000302 notificationsEnabled: boolean = false;
303
304 // Track if the window is focused to control notifications
305 @state()
306 private _windowFocused: boolean = document.hasFocus();
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000307
Sean McCullough86b56862025-04-18 13:04:03 -0700308 @property()
309 connectionErrorMessage: string = "";
310
Sean McCullough86b56862025-04-18 13:04:03 -0700311 // Chat messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100312 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700313 messages: AgentMessage[] = [];
Sean McCullough86b56862025-04-18 13:04:03 -0700314
315 @property()
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000316 set title(value: string) {
317 const oldValue = this._title;
318 this._title = value;
319 this.requestUpdate("title", oldValue);
320 // Update document title when title property changes
321 this.updateDocumentTitle();
322 }
323
324 get title(): string {
325 return this._title;
326 }
327
328 private _title: string = "";
Sean McCullough86b56862025-04-18 13:04:03 -0700329
330 private dataManager = new DataManager();
331
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100332 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700333 containerState: State = {
Philip Zeyligerd03318d2025-05-08 13:09:12 -0700334 state_version: 2,
Sean McCulloughd9f13372025-04-21 15:08:49 -0700335 title: "",
336 os: "",
337 message_count: 0,
338 hostname: "",
339 working_dir: "",
340 initial_commit: "",
Philip Zeyliger99a9a022025-04-27 15:15:25 +0000341 outstanding_llm_calls: 0,
342 outstanding_tool_calls: [],
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000343 session_id: "",
344 ssh_available: false,
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700345 ssh_error: "",
346 in_container: false,
347 first_message_index: 0,
Sean McCulloughd9f13372025-04-21 15:08:49 -0700348 };
Sean McCullough86b56862025-04-18 13:04:03 -0700349
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700350 @state()
351 private restartModalOpen = false;
352
Sean McCullough86b56862025-04-18 13:04:03 -0700353 // Mutation observer to detect when new messages are added
354 private mutationObserver: MutationObserver | null = null;
355
356 constructor() {
357 super();
358
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000359 // Reference to the container status element
360 this.containerStatusElement = null;
361
Sean McCullough86b56862025-04-18 13:04:03 -0700362 // Binding methods to this
363 this._handleViewModeSelect = this._handleViewModeSelect.bind(this);
Sean McCullough34bb09a2025-05-13 15:39:54 -0700364 this._handlePopState = this._handlePopState.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -0700365 this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
Sean McCullough485afc62025-04-28 14:28:39 -0700366 this._handleMutlipleChoiceSelected =
367 this._handleMutlipleChoiceSelected.bind(this);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000368 this._handleStopClick = this._handleStopClick.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000369 this._handleNotificationsToggle =
370 this._handleNotificationsToggle.bind(this);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000371 this._handleWindowFocus = this._handleWindowFocus.bind(this);
372 this._handleWindowBlur = this._handleWindowBlur.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000373
374 // Load notification preference from localStorage
375 try {
376 const savedPref = localStorage.getItem("sketch-notifications-enabled");
377 if (savedPref !== null) {
378 this.notificationsEnabled = savedPref === "true";
379 }
380 } catch (error) {
381 console.error("Error loading notification preference:", error);
382 }
Sean McCullough86b56862025-04-18 13:04:03 -0700383 }
384
385 // See https://lit.dev/docs/components/lifecycle/
386 connectedCallback() {
387 super.connectedCallback();
388
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000389 // Get reference to the container status element
390 setTimeout(() => {
391 this.containerStatusElement =
392 this.shadowRoot?.getElementById("container-status");
393 }, 0);
394
Sean McCullough86b56862025-04-18 13:04:03 -0700395 // Initialize client-side nav history.
396 const url = new URL(window.location.href);
397 const mode = url.searchParams.get("view") || "chat";
398 window.history.replaceState({ mode }, "", url.toString());
399
400 this.toggleViewMode(mode as ViewMode, false);
401 // Add popstate event listener to handle browser back/forward navigation
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100402 window.addEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700403
404 // Add event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100405 window.addEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100406 window.addEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700407
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000408 // Add window focus/blur listeners for controlling notifications
409 window.addEventListener("focus", this._handleWindowFocus);
410 window.addEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700411 window.addEventListener(
412 "multiple-choice-selected",
413 this._handleMutlipleChoiceSelected,
414 );
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000415
Sean McCullough86b56862025-04-18 13:04:03 -0700416 // register event listeners
417 this.dataManager.addEventListener(
418 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700419 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700420 );
421 this.dataManager.addEventListener(
422 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700423 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700424 );
425
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000426 // Set initial document title
427 this.updateDocumentTitle();
428
Sean McCullough86b56862025-04-18 13:04:03 -0700429 // Initialize the data manager
430 this.dataManager.initialize();
Autoformattercf570962025-04-30 17:27:39 +0000431
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000432 // Process existing messages for commit info
433 if (this.messages && this.messages.length > 0) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000434 // Update last commit info via container status component
435 setTimeout(() => {
436 if (this.containerStatusElement) {
437 this.containerStatusElement.updateLastCommitInfo(this.messages);
438 }
439 }, 100);
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000440 }
Sean McCullough86b56862025-04-18 13:04:03 -0700441 }
442
443 // See https://lit.dev/docs/components/lifecycle/
444 disconnectedCallback() {
445 super.disconnectedCallback();
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100446 window.removeEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700447
448 // Remove event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100449 window.removeEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100450 window.removeEventListener("show-commit-diff", this._handleShowCommitDiff);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000451 window.removeEventListener("focus", this._handleWindowFocus);
452 window.removeEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700453 window.removeEventListener(
454 "multiple-choice-selected",
455 this._handleMutlipleChoiceSelected,
456 );
Sean McCullough86b56862025-04-18 13:04:03 -0700457
458 // unregister data manager event listeners
459 this.dataManager.removeEventListener(
460 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700461 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700462 );
463 this.dataManager.removeEventListener(
464 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700465 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700466 );
467
468 // Disconnect mutation observer if it exists
469 if (this.mutationObserver) {
Sean McCullough86b56862025-04-18 13:04:03 -0700470 this.mutationObserver.disconnect();
471 this.mutationObserver = null;
472 }
473 }
474
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000475 updateUrlForViewMode(mode: "chat" | "diff" | "terminal"): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700476 // Get the current URL without search parameters
477 const url = new URL(window.location.href);
478
479 // Clear existing parameters
480 url.search = "";
481
482 // Only add view parameter if not in default chat view
483 if (mode !== "chat") {
484 url.searchParams.set("view", mode);
Sean McCullough71941bd2025-04-18 13:31:48 -0700485 const diffView = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700486 ".diff-view",
Sean McCullough71941bd2025-04-18 13:31:48 -0700487 ) as SketchDiffView;
Sean McCullough86b56862025-04-18 13:04:03 -0700488
489 // If in diff view and there's a commit hash, include that too
490 if (mode === "diff" && diffView.commitHash) {
491 url.searchParams.set("commit", diffView.commitHash);
492 }
493 }
494
495 // Update the browser history without reloading the page
496 window.history.pushState({ mode }, "", url.toString());
497 }
498
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100499 private _handlePopState(event: PopStateEvent) {
Sean McCullough86b56862025-04-18 13:04:03 -0700500 if (event.state && event.state.mode) {
501 this.toggleViewMode(event.state.mode, false);
502 } else {
503 this.toggleViewMode("chat", false);
504 }
505 }
506
507 /**
508 * Handle view mode selection event
509 */
510 private _handleViewModeSelect(event: CustomEvent) {
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000511 const mode = event.detail.mode as "chat" | "diff" | "terminal";
Sean McCullough86b56862025-04-18 13:04:03 -0700512 this.toggleViewMode(mode, true);
513 }
514
515 /**
516 * Handle show commit diff event
517 */
518 private _handleShowCommitDiff(event: CustomEvent) {
519 const { commitHash } = event.detail;
520 if (commitHash) {
521 this.showCommitDiff(commitHash);
522 }
523 }
524
Sean McCullough485afc62025-04-28 14:28:39 -0700525 private _handleMultipleChoice(event: CustomEvent) {
526 window.console.log("_handleMultipleChoice", event);
527 this._sendChat;
528 }
Sean McCullough86b56862025-04-18 13:04:03 -0700529 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700530 * Listen for commit diff event
531 * @param commitHash The commit hash to show diff for
532 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100533 private showCommitDiff(commitHash: string): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700534 // Store the commit hash
535 this.currentCommitHash = commitHash;
536
537 // Switch to diff view
Sean McCullough71941bd2025-04-18 13:31:48 -0700538 this.toggleViewMode("diff", true);
Sean McCullough86b56862025-04-18 13:04:03 -0700539
540 // Wait for DOM update to complete
541 this.updateComplete.then(() => {
542 // Get the diff view component
543 const diffView = this.shadowRoot?.querySelector("sketch-diff-view");
544 if (diffView) {
545 // Call the showCommitDiff method
546 (diffView as any).showCommitDiff(commitHash);
547 }
548 });
549 }
550
551 /**
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000552 * Toggle between different view modes: chat, diff, terminal
Sean McCullough86b56862025-04-18 13:04:03 -0700553 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100554 private toggleViewMode(mode: ViewMode, updateHistory: boolean): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700555 // Don't do anything if the mode is already active
556 if (this.viewMode === mode) return;
557
558 // Update the view mode
559 this.viewMode = mode;
560
561 if (updateHistory) {
562 // Update URL with the current view mode
563 this.updateUrlForViewMode(mode);
564 }
565
566 // Wait for DOM update to complete
567 this.updateComplete.then(() => {
568 // Update active view
Pokey Rule46fff972025-04-25 14:57:44 +0100569 const viewContainerInner = this.shadowRoot?.querySelector(
570 "#view-container-inner",
571 );
Sean McCullough86b56862025-04-18 13:04:03 -0700572 const chatView = this.shadowRoot?.querySelector(".chat-view");
573 const diffView = this.shadowRoot?.querySelector(".diff-view");
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000574
Sean McCullough86b56862025-04-18 13:04:03 -0700575 const terminalView = this.shadowRoot?.querySelector(".terminal-view");
576
577 // Remove active class from all views
578 chatView?.classList.remove("view-active");
579 diffView?.classList.remove("view-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700580 terminalView?.classList.remove("view-active");
581
582 // Add/remove diff-active class on view container
583 if (mode === "diff") {
Pokey Rule46fff972025-04-25 14:57:44 +0100584 viewContainerInner?.classList.add("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700585 } else {
Pokey Rule46fff972025-04-25 14:57:44 +0100586 viewContainerInner?.classList.remove("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700587 }
588
589 // Add active class to the selected view
590 switch (mode) {
591 case "chat":
592 chatView?.classList.add("view-active");
593 break;
594 case "diff":
595 diffView?.classList.add("view-active");
596 // Load diff content if we have a diff view
597 const diffViewComp =
598 this.shadowRoot?.querySelector("sketch-diff-view");
599 if (diffViewComp && this.currentCommitHash) {
600 (diffViewComp as any).showCommitDiff(this.currentCommitHash);
601 } else if (diffViewComp) {
602 (diffViewComp as any).loadDiffContent();
603 }
604 break;
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000605
Sean McCullough86b56862025-04-18 13:04:03 -0700606 case "terminal":
607 terminalView?.classList.add("view-active");
608 break;
609 }
610
611 // Update view mode buttons
612 const viewModeSelect = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700613 "sketch-view-mode-select",
Sean McCullough86b56862025-04-18 13:04:03 -0700614 );
615 if (viewModeSelect) {
616 const event = new CustomEvent("update-active-mode", {
617 detail: { mode },
618 bubbles: true,
619 composed: true,
620 });
621 viewModeSelect.dispatchEvent(event);
622 }
Sean McCullough86b56862025-04-18 13:04:03 -0700623 });
624 }
625
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000626 /**
627 * Updates the document title based on current title and connection status
628 */
629 private updateDocumentTitle(): void {
630 let docTitle = `sk: ${this.title || "untitled"}`;
631
632 // Add red circle emoji if disconnected
633 if (this.connectionStatus === "disconnected") {
634 docTitle += " 🔴";
635 }
636
637 document.title = docTitle;
638 }
639
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000640 // Check and request notification permission if needed
641 private async checkNotificationPermission(): Promise<boolean> {
642 // Check if the Notification API is supported
643 if (!("Notification" in window)) {
644 console.log("This browser does not support notifications");
645 return false;
646 }
647
648 // Check if permission is already granted
649 if (Notification.permission === "granted") {
650 return true;
651 }
652
653 // If permission is not denied, request it
654 if (Notification.permission !== "denied") {
655 const permission = await Notification.requestPermission();
656 return permission === "granted";
657 }
658
659 return false;
660 }
661
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000662 // Handle notifications toggle click
663 private _handleNotificationsToggle(): void {
664 this.notificationsEnabled = !this.notificationsEnabled;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000665
666 // If enabling notifications, check permissions
667 if (this.notificationsEnabled) {
668 this.checkNotificationPermission();
669 }
670
671 // Save preference to localStorage
672 try {
673 localStorage.setItem(
674 "sketch-notifications-enabled",
675 String(this.notificationsEnabled),
676 );
677 } catch (error) {
678 console.error("Error saving notification preference:", error);
679 }
680 }
681
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000682 // Handle window focus event
683 private _handleWindowFocus(): void {
684 this._windowFocused = true;
685 }
686
687 // Handle window blur event
688 private _handleWindowBlur(): void {
689 this._windowFocused = false;
690 }
691
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000692 // Show notification for message with EndOfTurn=true
693 private async showEndOfTurnNotification(
694 message: AgentMessage,
695 ): Promise<void> {
696 // Don't show notifications if they're disabled
697 if (!this.notificationsEnabled) return;
698
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000699 // Don't show notifications if the window is focused
700 if (this._windowFocused) return;
701
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000702 // Check if we have permission to show notifications
703 const hasPermission = await this.checkNotificationPermission();
704 if (!hasPermission) return;
705
Philip Zeyliger32011332025-04-30 20:59:40 +0000706 // Only show notifications for agent messages with end_of_turn=true and no parent_conversation_id
707 if (
708 message.type !== "agent" ||
709 !message.end_of_turn ||
710 message.parent_conversation_id
711 )
712 return;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000713
714 // Create a title that includes the sketch title
715 const notificationTitle = `Sketch: ${this.title || "untitled"}`;
716
717 // Extract the beginning of the message content (first 100 chars)
718 const messagePreview = message.content
719 ? message.content.substring(0, 100) +
720 (message.content.length > 100 ? "..." : "")
721 : "Agent has completed its turn";
722
723 // Create and show the notification
724 try {
725 new Notification(notificationTitle, {
726 body: messagePreview,
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000727 icon: "https://sketch.dev/favicon.ico", // Use sketch.dev favicon for notification
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000728 });
729 } catch (error) {
730 console.error("Error showing notification:", error);
731 }
732 }
733
Sean McCullough86b56862025-04-18 13:04:03 -0700734 private handleDataChanged(eventData: {
735 state: State;
Sean McCulloughd9f13372025-04-21 15:08:49 -0700736 newMessages: AgentMessage[];
Sean McCullough86b56862025-04-18 13:04:03 -0700737 }): void {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000738 const { state, newMessages } = eventData;
Sean McCullough86b56862025-04-18 13:04:03 -0700739
740 // Update state if we received it
741 if (state) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000742 // Ensure we're using the latest call status to prevent indicators from being stuck
Autoformatterf830c9d2025-04-30 18:16:01 +0000743 if (
744 state.outstanding_llm_calls === 0 &&
745 state.outstanding_tool_calls.length === 0
746 ) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000747 // Force reset containerState calls when nothing is reported as in progress
748 state.outstanding_llm_calls = 0;
749 state.outstanding_tool_calls = [];
750 }
Autoformatterf830c9d2025-04-30 18:16:01 +0000751
Sean McCullough86b56862025-04-18 13:04:03 -0700752 this.containerState = state;
753 this.title = state.title;
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000754
755 // Update document title when sketch title changes
756 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700757 }
758
Sean McCullough86b56862025-04-18 13:04:03 -0700759 // Update messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100760 this.messages = aggregateAgentMessages(this.messages, newMessages);
Autoformattercf570962025-04-30 17:27:39 +0000761
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000762 // Process new messages to find commit messages
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000763 // Update last commit info via container status component
764 if (this.containerStatusElement) {
765 this.containerStatusElement.updateLastCommitInfo(newMessages);
766 }
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000767
768 // Check for agent messages with end_of_turn=true and show notifications
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000769 if (newMessages && newMessages.length > 0) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000770 for (const message of newMessages) {
Philip Zeyliger32011332025-04-30 20:59:40 +0000771 if (
772 message.type === "agent" &&
773 message.end_of_turn &&
774 !message.parent_conversation_id
775 ) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000776 this.showEndOfTurnNotification(message);
777 break; // Only show one notification per batch of messages
778 }
779 }
780 }
Sean McCullough86b56862025-04-18 13:04:03 -0700781 }
782
783 private handleConnectionStatusChanged(
784 status: ConnectionStatus,
Philip Zeyliger72682df2025-04-23 13:09:46 -0700785 errorMessage?: string,
Sean McCullough86b56862025-04-18 13:04:03 -0700786 ): void {
787 this.connectionStatus = status;
788 this.connectionErrorMessage = errorMessage || "";
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000789
790 // Update document title when connection status changes
791 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700792 }
793
Sean McCulloughd3906e22025-04-29 17:32:14 +0000794 private async _handleStopClick(): Promise<void> {
795 try {
796 const response = await fetch("cancel", {
797 method: "POST",
798 headers: {
799 "Content-Type": "application/json",
800 },
801 body: JSON.stringify({ reason: "user requested cancellation" }),
802 });
803
804 if (!response.ok) {
805 const errorData = await response.text();
806 throw new Error(
807 `Failed to stop operation: ${response.status} - ${errorData}`,
808 );
809 }
810
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000811 // Stop request sent
Sean McCulloughd3906e22025-04-29 17:32:14 +0000812 } catch (error) {
813 console.error("Error stopping operation:", error);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000814 }
815 }
816
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700817 openRestartModal() {
818 this.restartModalOpen = true;
819 }
820
821 handleRestartModalClose() {
822 this.restartModalOpen = false;
823 }
824
Sean McCullough485afc62025-04-28 14:28:39 -0700825 async _handleMutlipleChoiceSelected(e: CustomEvent) {
826 const chatInput = this.shadowRoot?.querySelector(
827 "sketch-chat-input",
828 ) as SketchChatInput;
829 if (chatInput) {
830 chatInput.content = e.detail.responseText;
831 chatInput.focus();
832 }
833 }
834
Sean McCullough86b56862025-04-18 13:04:03 -0700835 async _sendChat(e: CustomEvent) {
836 console.log("app shell: _sendChat", e);
Sean McCullough485afc62025-04-28 14:28:39 -0700837 e.preventDefault();
838 e.stopPropagation();
Sean McCullough86b56862025-04-18 13:04:03 -0700839 const message = e.detail.message?.trim();
840 if (message == "") {
841 return;
842 }
843 try {
Josh Bleecher Snyder98b64d12025-05-12 19:42:43 +0000844 // Always switch to chat view when sending a message so user can see processing
845 if (this.viewMode !== "chat") {
846 this.toggleViewMode("chat", true);
847 }
Autoformatter5c7f9572025-05-13 01:17:31 +0000848
Sean McCullough86b56862025-04-18 13:04:03 -0700849 // Send the message to the server
850 const response = await fetch("chat", {
851 method: "POST",
852 headers: {
853 "Content-Type": "application/json",
854 },
855 body: JSON.stringify({ message }),
856 });
857
858 if (!response.ok) {
859 const errorData = await response.text();
860 throw new Error(`Server error: ${response.status} - ${errorData}`);
861 }
Sean McCullough86b56862025-04-18 13:04:03 -0700862 } catch (error) {
863 console.error("Error sending chat message:", error);
864 const statusText = document.getElementById("statusText");
865 if (statusText) {
866 statusText.textContent = "Error sending message";
867 }
868 }
869 }
870
Pokey Rule4097e532025-04-24 18:55:28 +0100871 private scrollContainerRef = createRef<HTMLElement>();
872
Sean McCullough86b56862025-04-18 13:04:03 -0700873 render() {
874 return html`
Pokey Rule4097e532025-04-24 18:55:28 +0100875 <div id="top-banner">
Sean McCullough86b56862025-04-18 13:04:03 -0700876 <div class="title-container">
877 <h1 class="banner-title">sketch</h1>
878 <h2 id="chatTitle" class="chat-title">${this.title}</h2>
879 </div>
880
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000881 <!-- Container status info moved above tabs -->
Sean McCullough86b56862025-04-18 13:04:03 -0700882 <sketch-container-status
883 .state=${this.containerState}
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000884 id="container-status"
Sean McCullough86b56862025-04-18 13:04:03 -0700885 ></sketch-container-status>
Autoformattercf570962025-04-30 17:27:39 +0000886
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000887 <!-- Last Commit section moved to sketch-container-status -->
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000888
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000889 <!-- Views section with tabs -->
890 <sketch-view-mode-select></sketch-view-mode-select>
Sean McCullough86b56862025-04-18 13:04:03 -0700891
892 <div class="refresh-control">
Sean McCulloughd3906e22025-04-29 17:32:14 +0000893 <button
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700894 id="restartButton"
895 class="restart-button"
896 ?disabled=${this.containerState.message_count === 0}
897 @click=${this.openRestartModal}
Sean McCulloughd3906e22025-04-29 17:32:14 +0000898 >
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000899 <svg
900 class="button-icon"
901 xmlns="http://www.w3.org/2000/svg"
902 viewBox="0 0 24 24"
903 fill="none"
904 stroke="currentColor"
905 stroke-width="2"
906 stroke-linecap="round"
907 stroke-linejoin="round"
908 >
909 <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
910 <path d="M3 3v5h5" />
911 </svg>
912 <span class="button-text">Restart</span>
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700913 </button>
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000914 <button
915 id="stopButton"
916 class="stop-button"
917 ?disabled=${(this.containerState?.outstanding_llm_calls || 0) ===
918 0 &&
919 (this.containerState?.outstanding_tool_calls || []).length === 0}
920 >
921 <svg
922 class="button-icon"
923 xmlns="http://www.w3.org/2000/svg"
924 viewBox="0 0 24 24"
925 fill="none"
926 stroke="currentColor"
927 stroke-width="2"
928 stroke-linecap="round"
929 stroke-linejoin="round"
930 >
931 <rect x="6" y="6" width="12" height="12" />
932 </svg>
933 <span class="button-text">Stop</span>
Sean McCullough86b56862025-04-18 13:04:03 -0700934 </button>
935
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000936 <div
937 class="notifications-toggle"
938 @click=${this._handleNotificationsToggle}
939 title="${this.notificationsEnabled
940 ? "Disable"
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000941 : "Enable"} notifications when the agent completes its turn"
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000942 >
943 <div
944 class="bell-icon ${!this.notificationsEnabled
945 ? "bell-disabled"
946 : ""}"
947 >
948 <!-- Bell SVG icon -->
949 <svg
950 xmlns="http://www.w3.org/2000/svg"
951 width="16"
952 height="16"
953 fill="currentColor"
954 viewBox="0 0 16 16"
955 >
956 <path
957 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"
958 />
959 </svg>
960 </div>
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000961 </div>
962
Philip Zeyliger99a9a022025-04-27 15:15:25 +0000963 <sketch-call-status
Sean McCulloughd9d45812025-04-30 16:53:41 -0700964 .agentState=${this.containerState?.agent_state}
Philip Zeyliger99a9a022025-04-27 15:15:25 +0000965 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
966 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
967 ></sketch-call-status>
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000968
969 <sketch-network-status
970 connection=${this.connectionStatus}
971 error=${this.connectionErrorMessage}
972 ></sketch-network-status>
Sean McCullough86b56862025-04-18 13:04:03 -0700973 </div>
974 </div>
975
Pokey Rule4097e532025-04-24 18:55:28 +0100976 <div id="view-container" ${ref(this.scrollContainerRef)}>
977 <div id="view-container-inner">
978 <div
979 class="chat-view ${this.viewMode === "chat" ? "view-active" : ""}"
980 >
981 <sketch-timeline
982 .messages=${this.messages}
983 .scrollContainer=${this.scrollContainerRef}
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000984 .agentState=${this.containerState?.agent_state}
985 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
986 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
Pokey Rule4097e532025-04-24 18:55:28 +0100987 ></sketch-timeline>
988 </div>
989 <div
990 class="diff-view ${this.viewMode === "diff" ? "view-active" : ""}"
991 >
992 <sketch-diff-view
993 .commitHash=${this.currentCommitHash}
994 ></sketch-diff-view>
995 </div>
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000996
Pokey Rule4097e532025-04-24 18:55:28 +0100997 <div
998 class="terminal-view ${this.viewMode === "terminal"
999 ? "view-active"
1000 : ""}"
1001 >
1002 <sketch-terminal></sketch-terminal>
1003 </div>
Sean McCullough86b56862025-04-18 13:04:03 -07001004 </div>
1005 </div>
1006
Pokey Rule4097e532025-04-24 18:55:28 +01001007 <div id="chat-input">
1008 <sketch-chat-input @send-chat="${this._sendChat}"></sketch-chat-input>
1009 </div>
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001010
1011 <sketch-restart-modal
1012 ?open=${this.restartModalOpen}
1013 @close=${this.handleRestartModalClose}
1014 .containerState=${this.containerState}
1015 .messages=${this.messages}
1016 ></sketch-restart-modal>
Sean McCullough86b56862025-04-18 13:04:03 -07001017 `;
1018 }
1019
1020 /**
Sean McCullough86b56862025-04-18 13:04:03 -07001021 * Lifecycle callback when component is first connected to DOM
1022 */
1023 firstUpdated(): void {
1024 if (this.viewMode !== "chat") {
1025 return;
1026 }
1027
1028 // Initial scroll to bottom when component is first rendered
1029 setTimeout(
1030 () => this.scrollTo({ top: this.scrollHeight, behavior: "smooth" }),
Philip Zeyliger72682df2025-04-23 13:09:46 -07001031 50,
Sean McCullough86b56862025-04-18 13:04:03 -07001032 );
1033
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001034 // Setup stop button
1035 const stopButton = this.renderRoot?.querySelector(
1036 "#stopButton",
1037 ) as HTMLButtonElement;
1038 stopButton?.addEventListener("click", async () => {
1039 try {
Sean McCullough495cb962025-05-01 16:25:53 -07001040 const response = await fetch("cancel", {
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001041 method: "POST",
1042 headers: {
1043 "Content-Type": "application/json",
1044 },
1045 body: JSON.stringify({ reason: "User clicked stop button" }),
1046 });
1047 if (!response.ok) {
1048 console.error("Failed to cancel:", await response.text());
1049 }
1050 } catch (error) {
1051 console.error("Error cancelling operation:", error);
1052 }
1053 });
1054
Philip Zeyliger47b71c92025-04-30 15:43:39 +00001055 // Process any existing messages to find commit information
1056 if (this.messages && this.messages.length > 0) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001057 // Update last commit info via container status component
1058 if (this.containerStatusElement) {
1059 this.containerStatusElement.updateLastCommitInfo(this.messages);
1060 }
Philip Zeyliger47b71c92025-04-30 15:43:39 +00001061 }
Sean McCullough86b56862025-04-18 13:04:03 -07001062 }
1063}
1064
1065declare global {
1066 interface HTMLElementTagNameMap {
1067 "sketch-app-shell": SketchAppShell;
1068 }
1069}