blob: d351339c84b2883e73e8e960a23814ff2886fb49 [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()
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,
Sean McCullough86b56862025-04-18 13:04:03 -0700200 .terminal-view {
201 display: none; /* Hidden by default */
202 width: 100%;
203 }
204
205 /* Active view styles - these will be applied via JavaScript */
206 .view-active {
207 display: flex;
208 flex-direction: column;
209 }
210
211 .title-container {
212 display: flex;
213 flex-direction: column;
214 white-space: nowrap;
215 overflow: hidden;
216 text-overflow: ellipsis;
Josh Bleecher Snydereb5166a2025-04-30 17:04:20 +0000217 max-width: 30%;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000218 padding: 6px 0;
Sean McCullough86b56862025-04-18 13:04:03 -0700219 }
220
221 .refresh-control {
222 display: flex;
223 align-items: center;
224 margin-bottom: 0;
225 flex-wrap: nowrap;
226 white-space: nowrap;
227 flex-shrink: 0;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000228 gap: 15px;
229 padding-left: 15px;
230 margin-right: 50px;
Sean McCullough86b56862025-04-18 13:04:03 -0700231 }
232
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000233 .restart-button,
234 .stop-button {
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700235 background: #2196f3;
236 color: white;
237 border: none;
238 padding: 4px 10px;
239 border-radius: 4px;
240 cursor: pointer;
241 font-size: 12px;
242 margin-right: 5px;
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000243 display: flex;
244 align-items: center;
245 gap: 6px;
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700246 }
247
248 .restart-button:hover {
249 background-color: #0b7dda;
250 }
251
252 .restart-button:disabled {
253 background-color: #ccc;
254 cursor: not-allowed;
255 opacity: 0.6;
256 }
257
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000258 .stop-button {
259 background: #dc3545;
Sean McCullough86b56862025-04-18 13:04:03 -0700260 color: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700261 }
262
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000263 .stop-button:hover:not(:disabled) {
264 background-color: #c82333;
Sean McCullough86b56862025-04-18 13:04:03 -0700265 }
266
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000267 .stop-button:disabled {
268 background-color: #e9a8ad;
269 cursor: not-allowed;
270 opacity: 0.7;
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000271 }
272
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000273 .stop-button:disabled:hover {
274 background-color: #e9a8ad;
275 }
276
277 .button-icon {
278 width: 16px;
279 height: 16px;
280 }
281
282 @media (max-width: 1400px) {
283 .button-text {
284 display: none;
285 }
286
287 .restart-button,
288 .stop-button {
289 padding: 6px;
290 }
291 }
292
293 /* Removed poll-updates class */
294
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000295 .notifications-toggle {
Sean McCullough86b56862025-04-18 13:04:03 -0700296 display: flex;
297 align-items: center;
Sean McCullough86b56862025-04-18 13:04:03 -0700298 font-size: 12px;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000299 margin-right: 10px;
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000300 cursor: pointer;
301 }
302
303 .bell-icon {
304 width: 20px;
305 height: 20px;
306 position: relative;
307 display: inline-flex;
308 align-items: center;
309 justify-content: center;
310 }
311
312 .bell-disabled::before {
313 content: "";
314 position: absolute;
315 width: 2px;
316 height: 24px;
317 background-color: #dc3545;
318 transform: rotate(45deg);
319 transform-origin: center center;
Sean McCullough86b56862025-04-18 13:04:03 -0700320 }
321 `;
322
323 // Header bar: Network connection status details
324 @property()
325 connectionStatus: ConnectionStatus = "disconnected";
Autoformattercf570962025-04-30 17:27:39 +0000326
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000327 // Track if the last commit info has been copied
328 @state()
329 lastCommitCopied: boolean = false;
Sean McCullough86b56862025-04-18 13:04:03 -0700330
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000331 // Track notification preferences
332 @state()
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000333 notificationsEnabled: boolean = false;
334
335 // Track if the window is focused to control notifications
336 @state()
337 private _windowFocused: boolean = document.hasFocus();
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000338
Sean McCullough86b56862025-04-18 13:04:03 -0700339 @property()
340 connectionErrorMessage: string = "";
341
Sean McCullough86b56862025-04-18 13:04:03 -0700342 // Chat messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100343 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700344 messages: AgentMessage[] = [];
Sean McCullough86b56862025-04-18 13:04:03 -0700345
346 @property()
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000347 set title(value: string) {
348 const oldValue = this._title;
349 this._title = value;
350 this.requestUpdate("title", oldValue);
351 // Update document title when title property changes
352 this.updateDocumentTitle();
353 }
354
355 get title(): string {
356 return this._title;
357 }
358
359 private _title: string = "";
Sean McCullough86b56862025-04-18 13:04:03 -0700360
361 private dataManager = new DataManager();
362
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100363 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700364 containerState: State = {
365 title: "",
366 os: "",
367 message_count: 0,
368 hostname: "",
369 working_dir: "",
370 initial_commit: "",
Philip Zeyliger99a9a022025-04-27 15:15:25 +0000371 outstanding_llm_calls: 0,
372 outstanding_tool_calls: [],
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000373 session_id: "",
374 ssh_available: false,
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700375 ssh_error: "",
376 in_container: false,
377 first_message_index: 0,
Sean McCulloughd9f13372025-04-21 15:08:49 -0700378 };
Sean McCullough86b56862025-04-18 13:04:03 -0700379
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700380 @state()
381 private restartModalOpen = false;
382
Sean McCullough86b56862025-04-18 13:04:03 -0700383 // Mutation observer to detect when new messages are added
384 private mutationObserver: MutationObserver | null = null;
385
386 constructor() {
387 super();
388
389 // Binding methods to this
390 this._handleViewModeSelect = this._handleViewModeSelect.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -0700391 this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
Sean McCullough485afc62025-04-28 14:28:39 -0700392 this._handleMutlipleChoiceSelected =
393 this._handleMutlipleChoiceSelected.bind(this);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000394 this._handleStopClick = this._handleStopClick.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000395 this._handleNotificationsToggle =
396 this._handleNotificationsToggle.bind(this);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000397 this._handleWindowFocus = this._handleWindowFocus.bind(this);
398 this._handleWindowBlur = this._handleWindowBlur.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000399
400 // Load notification preference from localStorage
401 try {
402 const savedPref = localStorage.getItem("sketch-notifications-enabled");
403 if (savedPref !== null) {
404 this.notificationsEnabled = savedPref === "true";
405 }
406 } catch (error) {
407 console.error("Error loading notification preference:", error);
408 }
Sean McCullough86b56862025-04-18 13:04:03 -0700409 }
410
411 // See https://lit.dev/docs/components/lifecycle/
412 connectedCallback() {
413 super.connectedCallback();
414
415 // Initialize client-side nav history.
416 const url = new URL(window.location.href);
417 const mode = url.searchParams.get("view") || "chat";
418 window.history.replaceState({ mode }, "", url.toString());
419
420 this.toggleViewMode(mode as ViewMode, false);
421 // Add popstate event listener to handle browser back/forward navigation
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100422 window.addEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700423
424 // Add event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100425 window.addEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100426 window.addEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700427
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000428 // Add window focus/blur listeners for controlling notifications
429 window.addEventListener("focus", this._handleWindowFocus);
430 window.addEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700431 window.addEventListener(
432 "multiple-choice-selected",
433 this._handleMutlipleChoiceSelected,
434 );
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000435
Sean McCullough86b56862025-04-18 13:04:03 -0700436 // register event listeners
437 this.dataManager.addEventListener(
438 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700439 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700440 );
441 this.dataManager.addEventListener(
442 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700443 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700444 );
445
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000446 // Set initial document title
447 this.updateDocumentTitle();
448
Sean McCullough86b56862025-04-18 13:04:03 -0700449 // Initialize the data manager
450 this.dataManager.initialize();
Autoformattercf570962025-04-30 17:27:39 +0000451
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000452 // Process existing messages for commit info
453 if (this.messages && this.messages.length > 0) {
454 this.updateLastCommitInfo(this.messages);
455 }
Sean McCullough86b56862025-04-18 13:04:03 -0700456 }
457
458 // See https://lit.dev/docs/components/lifecycle/
459 disconnectedCallback() {
460 super.disconnectedCallback();
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100461 window.removeEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700462
463 // Remove event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100464 window.removeEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100465 window.removeEventListener("show-commit-diff", this._handleShowCommitDiff);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000466 window.removeEventListener("focus", this._handleWindowFocus);
467 window.removeEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700468 window.removeEventListener(
469 "multiple-choice-selected",
470 this._handleMutlipleChoiceSelected,
471 );
Sean McCullough86b56862025-04-18 13:04:03 -0700472
473 // unregister data manager event listeners
474 this.dataManager.removeEventListener(
475 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700476 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700477 );
478 this.dataManager.removeEventListener(
479 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700480 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700481 );
482
483 // Disconnect mutation observer if it exists
484 if (this.mutationObserver) {
Sean McCullough86b56862025-04-18 13:04:03 -0700485 this.mutationObserver.disconnect();
486 this.mutationObserver = null;
487 }
488 }
489
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000490 updateUrlForViewMode(mode: "chat" | "diff" | "terminal"): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700491 // Get the current URL without search parameters
492 const url = new URL(window.location.href);
493
494 // Clear existing parameters
495 url.search = "";
496
497 // Only add view parameter if not in default chat view
498 if (mode !== "chat") {
499 url.searchParams.set("view", mode);
Sean McCullough71941bd2025-04-18 13:31:48 -0700500 const diffView = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700501 ".diff-view",
Sean McCullough71941bd2025-04-18 13:31:48 -0700502 ) as SketchDiffView;
Sean McCullough86b56862025-04-18 13:04:03 -0700503
504 // If in diff view and there's a commit hash, include that too
505 if (mode === "diff" && diffView.commitHash) {
506 url.searchParams.set("commit", diffView.commitHash);
507 }
508 }
509
510 // Update the browser history without reloading the page
511 window.history.pushState({ mode }, "", url.toString());
512 }
513
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100514 private _handlePopState(event: PopStateEvent) {
Sean McCullough86b56862025-04-18 13:04:03 -0700515 if (event.state && event.state.mode) {
516 this.toggleViewMode(event.state.mode, false);
517 } else {
518 this.toggleViewMode("chat", false);
519 }
520 }
521
522 /**
523 * Handle view mode selection event
524 */
525 private _handleViewModeSelect(event: CustomEvent) {
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000526 const mode = event.detail.mode as "chat" | "diff" | "terminal";
Sean McCullough86b56862025-04-18 13:04:03 -0700527 this.toggleViewMode(mode, true);
528 }
529
530 /**
531 * Handle show commit diff event
532 */
533 private _handleShowCommitDiff(event: CustomEvent) {
534 const { commitHash } = event.detail;
535 if (commitHash) {
536 this.showCommitDiff(commitHash);
537 }
538 }
539
Sean McCullough485afc62025-04-28 14:28:39 -0700540 private _handleMultipleChoice(event: CustomEvent) {
541 window.console.log("_handleMultipleChoice", event);
542 this._sendChat;
543 }
Sean McCullough86b56862025-04-18 13:04:03 -0700544 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700545 * Listen for commit diff event
546 * @param commitHash The commit hash to show diff for
547 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100548 private showCommitDiff(commitHash: string): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700549 // Store the commit hash
550 this.currentCommitHash = commitHash;
551
552 // Switch to diff view
Sean McCullough71941bd2025-04-18 13:31:48 -0700553 this.toggleViewMode("diff", true);
Sean McCullough86b56862025-04-18 13:04:03 -0700554
555 // Wait for DOM update to complete
556 this.updateComplete.then(() => {
557 // Get the diff view component
558 const diffView = this.shadowRoot?.querySelector("sketch-diff-view");
559 if (diffView) {
560 // Call the showCommitDiff method
561 (diffView as any).showCommitDiff(commitHash);
562 }
563 });
564 }
565
566 /**
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000567 * Toggle between different view modes: chat, diff, terminal
Sean McCullough86b56862025-04-18 13:04:03 -0700568 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100569 private toggleViewMode(mode: ViewMode, updateHistory: boolean): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700570 // Don't do anything if the mode is already active
571 if (this.viewMode === mode) return;
572
573 // Update the view mode
574 this.viewMode = mode;
575
576 if (updateHistory) {
577 // Update URL with the current view mode
578 this.updateUrlForViewMode(mode);
579 }
580
581 // Wait for DOM update to complete
582 this.updateComplete.then(() => {
583 // Update active view
Pokey Rule46fff972025-04-25 14:57:44 +0100584 const viewContainerInner = this.shadowRoot?.querySelector(
585 "#view-container-inner",
586 );
Sean McCullough86b56862025-04-18 13:04:03 -0700587 const chatView = this.shadowRoot?.querySelector(".chat-view");
588 const diffView = this.shadowRoot?.querySelector(".diff-view");
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000589
Sean McCullough86b56862025-04-18 13:04:03 -0700590 const terminalView = this.shadowRoot?.querySelector(".terminal-view");
591
592 // Remove active class from all views
593 chatView?.classList.remove("view-active");
594 diffView?.classList.remove("view-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700595 terminalView?.classList.remove("view-active");
596
597 // Add/remove diff-active class on view container
598 if (mode === "diff") {
Pokey Rule46fff972025-04-25 14:57:44 +0100599 viewContainerInner?.classList.add("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700600 } else {
Pokey Rule46fff972025-04-25 14:57:44 +0100601 viewContainerInner?.classList.remove("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700602 }
603
604 // Add active class to the selected view
605 switch (mode) {
606 case "chat":
607 chatView?.classList.add("view-active");
608 break;
609 case "diff":
610 diffView?.classList.add("view-active");
611 // Load diff content if we have a diff view
612 const diffViewComp =
613 this.shadowRoot?.querySelector("sketch-diff-view");
614 if (diffViewComp && this.currentCommitHash) {
615 (diffViewComp as any).showCommitDiff(this.currentCommitHash);
616 } else if (diffViewComp) {
617 (diffViewComp as any).loadDiffContent();
618 }
619 break;
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000620
Sean McCullough86b56862025-04-18 13:04:03 -0700621 case "terminal":
622 terminalView?.classList.add("view-active");
623 break;
624 }
625
626 // Update view mode buttons
627 const viewModeSelect = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700628 "sketch-view-mode-select",
Sean McCullough86b56862025-04-18 13:04:03 -0700629 );
630 if (viewModeSelect) {
631 const event = new CustomEvent("update-active-mode", {
632 detail: { mode },
633 bubbles: true,
634 composed: true,
635 });
636 viewModeSelect.dispatchEvent(event);
637 }
Sean McCullough86b56862025-04-18 13:04:03 -0700638 });
639 }
640
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000641 /**
642 * Updates the document title based on current title and connection status
643 */
644 private updateDocumentTitle(): void {
645 let docTitle = `sk: ${this.title || "untitled"}`;
646
647 // Add red circle emoji if disconnected
648 if (this.connectionStatus === "disconnected") {
649 docTitle += " 🔴";
650 }
651
652 document.title = docTitle;
653 }
654
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000655 // Check and request notification permission if needed
656 private async checkNotificationPermission(): Promise<boolean> {
657 // Check if the Notification API is supported
658 if (!("Notification" in window)) {
659 console.log("This browser does not support notifications");
660 return false;
661 }
662
663 // Check if permission is already granted
664 if (Notification.permission === "granted") {
665 return true;
666 }
667
668 // If permission is not denied, request it
669 if (Notification.permission !== "denied") {
670 const permission = await Notification.requestPermission();
671 return permission === "granted";
672 }
673
674 return false;
675 }
676
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000677 // Handle notifications toggle click
678 private _handleNotificationsToggle(): void {
679 this.notificationsEnabled = !this.notificationsEnabled;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000680
681 // If enabling notifications, check permissions
682 if (this.notificationsEnabled) {
683 this.checkNotificationPermission();
684 }
685
686 // Save preference to localStorage
687 try {
688 localStorage.setItem(
689 "sketch-notifications-enabled",
690 String(this.notificationsEnabled),
691 );
692 } catch (error) {
693 console.error("Error saving notification preference:", error);
694 }
695 }
696
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000697 // Handle window focus event
698 private _handleWindowFocus(): void {
699 this._windowFocused = true;
700 }
701
702 // Handle window blur event
703 private _handleWindowBlur(): void {
704 this._windowFocused = false;
705 }
706
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000707 // Show notification for message with EndOfTurn=true
708 private async showEndOfTurnNotification(
709 message: AgentMessage,
710 ): Promise<void> {
711 // Don't show notifications if they're disabled
712 if (!this.notificationsEnabled) return;
713
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000714 // Don't show notifications if the window is focused
715 if (this._windowFocused) return;
716
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000717 // Check if we have permission to show notifications
718 const hasPermission = await this.checkNotificationPermission();
719 if (!hasPermission) return;
720
Philip Zeyliger32011332025-04-30 20:59:40 +0000721 // Only show notifications for agent messages with end_of_turn=true and no parent_conversation_id
722 if (
723 message.type !== "agent" ||
724 !message.end_of_turn ||
725 message.parent_conversation_id
726 )
727 return;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000728
729 // Create a title that includes the sketch title
730 const notificationTitle = `Sketch: ${this.title || "untitled"}`;
731
732 // Extract the beginning of the message content (first 100 chars)
733 const messagePreview = message.content
734 ? message.content.substring(0, 100) +
735 (message.content.length > 100 ? "..." : "")
736 : "Agent has completed its turn";
737
738 // Create and show the notification
739 try {
740 new Notification(notificationTitle, {
741 body: messagePreview,
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000742 icon: "https://sketch.dev/favicon.ico", // Use sketch.dev favicon for notification
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000743 });
744 } catch (error) {
745 console.error("Error showing notification:", error);
746 }
747 }
748
Sean McCullough86b56862025-04-18 13:04:03 -0700749 private handleDataChanged(eventData: {
750 state: State;
Sean McCulloughd9f13372025-04-21 15:08:49 -0700751 newMessages: AgentMessage[];
Sean McCullough86b56862025-04-18 13:04:03 -0700752 }): void {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000753 const { state, newMessages } = eventData;
Sean McCullough86b56862025-04-18 13:04:03 -0700754
755 // Update state if we received it
756 if (state) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000757 // Ensure we're using the latest call status to prevent indicators from being stuck
Autoformatterf830c9d2025-04-30 18:16:01 +0000758 if (
759 state.outstanding_llm_calls === 0 &&
760 state.outstanding_tool_calls.length === 0
761 ) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000762 // Force reset containerState calls when nothing is reported as in progress
763 state.outstanding_llm_calls = 0;
764 state.outstanding_tool_calls = [];
765 }
Autoformatterf830c9d2025-04-30 18:16:01 +0000766
Sean McCullough86b56862025-04-18 13:04:03 -0700767 this.containerState = state;
768 this.title = state.title;
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000769
770 // Update document title when sketch title changes
771 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700772 }
773
Sean McCullough86b56862025-04-18 13:04:03 -0700774 // Update messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100775 this.messages = aggregateAgentMessages(this.messages, newMessages);
Autoformattercf570962025-04-30 17:27:39 +0000776
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000777 // Process new messages to find commit messages
778 this.updateLastCommitInfo(newMessages);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000779
780 // Check for agent messages with end_of_turn=true and show notifications
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000781 if (newMessages && newMessages.length > 0) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000782 for (const message of newMessages) {
Philip Zeyliger32011332025-04-30 20:59:40 +0000783 if (
784 message.type === "agent" &&
785 message.end_of_turn &&
786 !message.parent_conversation_id
787 ) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000788 this.showEndOfTurnNotification(message);
789 break; // Only show one notification per batch of messages
790 }
791 }
792 }
Sean McCullough86b56862025-04-18 13:04:03 -0700793 }
794
795 private handleConnectionStatusChanged(
796 status: ConnectionStatus,
Philip Zeyliger72682df2025-04-23 13:09:46 -0700797 errorMessage?: string,
Sean McCullough86b56862025-04-18 13:04:03 -0700798 ): void {
799 this.connectionStatus = status;
800 this.connectionErrorMessage = errorMessage || "";
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000801
802 // Update document title when connection status changes
803 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700804 }
805
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000806 // Update last commit information when new messages arrive
807 private updateLastCommitInfo(newMessages: AgentMessage[]): void {
808 if (!newMessages || newMessages.length === 0) return;
Autoformattercf570962025-04-30 17:27:39 +0000809
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000810 // Process messages in chronological order (latest last)
811 for (const message of newMessages) {
Autoformattercf570962025-04-30 17:27:39 +0000812 if (
813 message.type === "commit" &&
814 message.commits &&
815 message.commits.length > 0
816 ) {
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000817 // Get the first commit from the list
818 const commit = message.commits[0];
819 if (commit) {
820 this.lastCommit = {
821 hash: commit.hash,
Autoformattercf570962025-04-30 17:27:39 +0000822 pushedBranch: commit.pushed_branch,
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000823 };
824 this.lastCommitCopied = false;
825 }
826 }
827 }
828 }
829
830 // Copy commit info to clipboard
831 private copyCommitInfo(event: MouseEvent): void {
832 event.preventDefault();
833 event.stopPropagation();
Autoformattercf570962025-04-30 17:27:39 +0000834
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000835 if (!this.lastCommit) return;
Autoformattercf570962025-04-30 17:27:39 +0000836
837 const textToCopy =
838 this.lastCommit.pushedBranch || this.lastCommit.hash.substring(0, 8);
839
840 navigator.clipboard
841 .writeText(textToCopy)
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000842 .then(() => {
843 this.lastCommitCopied = true;
844 // Reset the copied state after 2 seconds
845 setTimeout(() => {
846 this.lastCommitCopied = false;
847 }, 2000);
848 })
Autoformattercf570962025-04-30 17:27:39 +0000849 .catch((err) => {
850 console.error("Failed to copy commit info:", err);
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000851 });
852 }
Autoformattercf570962025-04-30 17:27:39 +0000853
Sean McCulloughd3906e22025-04-29 17:32:14 +0000854 private async _handleStopClick(): Promise<void> {
855 try {
856 const response = await fetch("cancel", {
857 method: "POST",
858 headers: {
859 "Content-Type": "application/json",
860 },
861 body: JSON.stringify({ reason: "user requested cancellation" }),
862 });
863
864 if (!response.ok) {
865 const errorData = await response.text();
866 throw new Error(
867 `Failed to stop operation: ${response.status} - ${errorData}`,
868 );
869 }
870
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000871 // Stop request sent
Sean McCulloughd3906e22025-04-29 17:32:14 +0000872 } catch (error) {
873 console.error("Error stopping operation:", error);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000874 }
875 }
876
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700877 openRestartModal() {
878 this.restartModalOpen = true;
879 }
880
881 handleRestartModalClose() {
882 this.restartModalOpen = false;
883 }
884
Sean McCullough485afc62025-04-28 14:28:39 -0700885 async _handleMutlipleChoiceSelected(e: CustomEvent) {
886 const chatInput = this.shadowRoot?.querySelector(
887 "sketch-chat-input",
888 ) as SketchChatInput;
889 if (chatInput) {
890 chatInput.content = e.detail.responseText;
891 chatInput.focus();
892 }
893 }
894
Sean McCullough86b56862025-04-18 13:04:03 -0700895 async _sendChat(e: CustomEvent) {
896 console.log("app shell: _sendChat", e);
Sean McCullough485afc62025-04-28 14:28:39 -0700897 e.preventDefault();
898 e.stopPropagation();
Sean McCullough86b56862025-04-18 13:04:03 -0700899 const message = e.detail.message?.trim();
900 if (message == "") {
901 return;
902 }
903 try {
904 // Send the message to the server
905 const response = await fetch("chat", {
906 method: "POST",
907 headers: {
908 "Content-Type": "application/json",
909 },
910 body: JSON.stringify({ message }),
911 });
912
913 if (!response.ok) {
914 const errorData = await response.text();
915 throw new Error(`Server error: ${response.status} - ${errorData}`);
916 }
Sean McCullough86b56862025-04-18 13:04:03 -0700917 } catch (error) {
918 console.error("Error sending chat message:", error);
919 const statusText = document.getElementById("statusText");
920 if (statusText) {
921 statusText.textContent = "Error sending message";
922 }
923 }
924 }
925
Pokey Rule4097e532025-04-24 18:55:28 +0100926 private scrollContainerRef = createRef<HTMLElement>();
927
Sean McCullough86b56862025-04-18 13:04:03 -0700928 render() {
929 return html`
Pokey Rule4097e532025-04-24 18:55:28 +0100930 <div id="top-banner">
Sean McCullough86b56862025-04-18 13:04:03 -0700931 <div class="title-container">
932 <h1 class="banner-title">sketch</h1>
933 <h2 id="chatTitle" class="chat-title">${this.title}</h2>
934 </div>
935
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000936 <!-- Container status info moved above tabs -->
Sean McCullough86b56862025-04-18 13:04:03 -0700937 <sketch-container-status
938 .state=${this.containerState}
939 ></sketch-container-status>
Autoformattercf570962025-04-30 17:27:39 +0000940
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000941 <!-- Views section with tabs - repositioned -->
942 <sketch-view-mode-select></sketch-view-mode-select>
943
Autoformattercf570962025-04-30 17:27:39 +0000944 ${this.lastCommit
945 ? html`
946 <div
947 class="last-commit"
948 @click=${(e: MouseEvent) => this.copyCommitInfo(e)}
949 title="Click to copy"
950 >
951 ${this.lastCommitCopied
952 ? html`<span class="copied-indicator">Copied!</span>`
953 : ""}
954 ${this.lastCommit.pushedBranch
955 ? html`<span class="commit-branch-indicator"
956 >${this.lastCommit.pushedBranch}</span
957 >`
958 : html`<span class="commit-hash-indicator"
959 >${this.lastCommit.hash.substring(0, 8)}</span
960 >`}
961 </div>
962 `
963 : ""}
Sean McCullough86b56862025-04-18 13:04:03 -0700964
965 <div class="refresh-control">
Sean McCulloughd3906e22025-04-29 17:32:14 +0000966 <button
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700967 id="restartButton"
968 class="restart-button"
969 ?disabled=${this.containerState.message_count === 0}
970 @click=${this.openRestartModal}
Sean McCulloughd3906e22025-04-29 17:32:14 +0000971 >
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000972 <svg
973 class="button-icon"
974 xmlns="http://www.w3.org/2000/svg"
975 viewBox="0 0 24 24"
976 fill="none"
977 stroke="currentColor"
978 stroke-width="2"
979 stroke-linecap="round"
980 stroke-linejoin="round"
981 >
982 <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
983 <path d="M3 3v5h5" />
984 </svg>
985 <span class="button-text">Restart</span>
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700986 </button>
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000987 <button
988 id="stopButton"
989 class="stop-button"
990 ?disabled=${(this.containerState?.outstanding_llm_calls || 0) ===
991 0 &&
992 (this.containerState?.outstanding_tool_calls || []).length === 0}
993 >
994 <svg
995 class="button-icon"
996 xmlns="http://www.w3.org/2000/svg"
997 viewBox="0 0 24 24"
998 fill="none"
999 stroke="currentColor"
1000 stroke-width="2"
1001 stroke-linecap="round"
1002 stroke-linejoin="round"
1003 >
1004 <rect x="6" y="6" width="12" height="12" />
1005 </svg>
1006 <span class="button-text">Stop</span>
Sean McCullough86b56862025-04-18 13:04:03 -07001007 </button>
1008
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +00001009 <div
1010 class="notifications-toggle"
1011 @click=${this._handleNotificationsToggle}
1012 title="${this.notificationsEnabled
1013 ? "Disable"
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001014 : "Enable"} notifications when the agent completes its turn"
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +00001015 >
1016 <div
1017 class="bell-icon ${!this.notificationsEnabled
1018 ? "bell-disabled"
1019 : ""}"
1020 >
1021 <!-- Bell SVG icon -->
1022 <svg
1023 xmlns="http://www.w3.org/2000/svg"
1024 width="16"
1025 height="16"
1026 fill="currentColor"
1027 viewBox="0 0 16 16"
1028 >
1029 <path
1030 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"
1031 />
1032 </svg>
1033 </div>
Philip Zeyligerbc6b6292025-04-30 18:00:15 +00001034 </div>
1035
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001036 <sketch-call-status
Sean McCulloughd9d45812025-04-30 16:53:41 -07001037 .agentState=${this.containerState?.agent_state}
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001038 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
1039 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
1040 ></sketch-call-status>
Philip Zeyliger25f6ff12025-05-02 04:24:10 +00001041
1042 <sketch-network-status
1043 connection=${this.connectionStatus}
1044 error=${this.connectionErrorMessage}
1045 ></sketch-network-status>
Sean McCullough86b56862025-04-18 13:04:03 -07001046 </div>
1047 </div>
1048
Pokey Rule4097e532025-04-24 18:55:28 +01001049 <div id="view-container" ${ref(this.scrollContainerRef)}>
1050 <div id="view-container-inner">
1051 <div
1052 class="chat-view ${this.viewMode === "chat" ? "view-active" : ""}"
1053 >
1054 <sketch-timeline
1055 .messages=${this.messages}
1056 .scrollContainer=${this.scrollContainerRef}
1057 ></sketch-timeline>
1058 </div>
1059 <div
1060 class="diff-view ${this.viewMode === "diff" ? "view-active" : ""}"
1061 >
1062 <sketch-diff-view
1063 .commitHash=${this.currentCommitHash}
1064 ></sketch-diff-view>
1065 </div>
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +00001066
Pokey Rule4097e532025-04-24 18:55:28 +01001067 <div
1068 class="terminal-view ${this.viewMode === "terminal"
1069 ? "view-active"
1070 : ""}"
1071 >
1072 <sketch-terminal></sketch-terminal>
1073 </div>
Sean McCullough86b56862025-04-18 13:04:03 -07001074 </div>
1075 </div>
1076
Pokey Rule4097e532025-04-24 18:55:28 +01001077 <div id="chat-input">
1078 <sketch-chat-input @send-chat="${this._sendChat}"></sketch-chat-input>
1079 </div>
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001080
1081 <sketch-restart-modal
1082 ?open=${this.restartModalOpen}
1083 @close=${this.handleRestartModalClose}
1084 .containerState=${this.containerState}
1085 .messages=${this.messages}
1086 ></sketch-restart-modal>
Sean McCullough86b56862025-04-18 13:04:03 -07001087 `;
1088 }
1089
1090 /**
Sean McCullough86b56862025-04-18 13:04:03 -07001091 * Lifecycle callback when component is first connected to DOM
1092 */
1093 firstUpdated(): void {
1094 if (this.viewMode !== "chat") {
1095 return;
1096 }
1097
1098 // Initial scroll to bottom when component is first rendered
1099 setTimeout(
1100 () => this.scrollTo({ top: this.scrollHeight, behavior: "smooth" }),
Philip Zeyliger72682df2025-04-23 13:09:46 -07001101 50,
Sean McCullough86b56862025-04-18 13:04:03 -07001102 );
1103
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001104 // Setup stop button
1105 const stopButton = this.renderRoot?.querySelector(
1106 "#stopButton",
1107 ) as HTMLButtonElement;
1108 stopButton?.addEventListener("click", async () => {
1109 try {
Sean McCullough495cb962025-05-01 16:25:53 -07001110 const response = await fetch("cancel", {
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001111 method: "POST",
1112 headers: {
1113 "Content-Type": "application/json",
1114 },
1115 body: JSON.stringify({ reason: "User clicked stop button" }),
1116 });
1117 if (!response.ok) {
1118 console.error("Failed to cancel:", await response.text());
1119 }
1120 } catch (error) {
1121 console.error("Error cancelling operation:", error);
1122 }
1123 });
1124
Philip Zeyliger47b71c92025-04-30 15:43:39 +00001125 // Process any existing messages to find commit information
1126 if (this.messages && this.messages.length > 0) {
1127 this.updateLastCommitInfo(this.messages);
1128 }
Sean McCullough86b56862025-04-18 13:04:03 -07001129 }
1130}
1131
1132declare global {
1133 interface HTMLElementTagNameMap {
1134 "sketch-app-shell": SketchAppShell;
1135 }
1136}