blob: 7b95bdd89a621ba8005a38687d3ecfd458425550 [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 Zeyliger272a90e2025-05-16 14:49:51 -07004import { AgentMessage, GitLogEntry, 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";
Philip Zeyliger272a90e2025-05-16 14:49:51 -070011import "./sketch-diff2-view";
12import { SketchDiff2View } from "./sketch-diff2-view";
13import { DefaultGitDataService } from "./git-data-service";
14import "./sketch-monaco-view";
Pokey Rule4097e532025-04-24 18:55:28 +010015import "./sketch-network-status";
Philip Zeyliger99a9a022025-04-27 15:15:25 +000016import "./sketch-call-status";
Pokey Rule4097e532025-04-24 18:55:28 +010017import "./sketch-terminal";
18import "./sketch-timeline";
19import "./sketch-view-mode-select";
Philip Zeyliger2c4db092025-04-28 16:57:50 -070020import "./sketch-restart-modal";
Pokey Rule4097e532025-04-24 18:55:28 +010021
22import { createRef, ref } from "lit/directives/ref.js";
Sean McCullough485afc62025-04-28 14:28:39 -070023import { SketchChatInput } from "./sketch-chat-input";
Sean McCullough86b56862025-04-18 13:04:03 -070024
Philip Zeyliger272a90e2025-05-16 14:49:51 -070025type ViewMode = "chat" | "diff" | "diff2" | "terminal";
Sean McCullough86b56862025-04-18 13:04:03 -070026
27@customElement("sketch-app-shell")
28export class SketchAppShell extends LitElement {
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +000029 // Current view mode (chat, diff, terminal)
Sean McCullough86b56862025-04-18 13:04:03 -070030 @state()
Philip Zeyliger272a90e2025-05-16 14:49:51 -070031 viewMode: ViewMode = "chat";
Sean McCullough86b56862025-04-18 13:04:03 -070032
33 // Current commit hash for diff view
34 @state()
35 currentCommitHash: string = "";
36
Philip Zeyliger47b71c92025-04-30 15:43:39 +000037 // Last commit information
38 @state()
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000039
40 // Reference to the container status element
41 containerStatusElement: any = null;
Philip Zeyliger47b71c92025-04-30 15:43:39 +000042
Sean McCullough86b56862025-04-18 13:04:03 -070043 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
44 // Note that these styles only apply to the scope of this web component's
45 // shadow DOM node, so they won't leak out or collide with CSS declared in
46 // other components or the containing web page (...unless you want it to do that).
47 static styles = css`
Philip Zeyliger47b71c92025-04-30 15:43:39 +000048 .copied-indicator {
49 position: absolute;
50 top: -20px;
51 left: 50%;
52 transform: translateX(-50%);
53 background: rgba(40, 167, 69, 0.9);
54 color: white;
55 padding: 2px 6px;
56 border-radius: 3px;
57 font-size: 10px;
58 font-family: system-ui, sans-serif;
59 animation: fadeInOut 2s ease;
60 pointer-events: none;
61 }
Autoformattercf570962025-04-30 17:27:39 +000062
Philip Zeyliger47b71c92025-04-30 15:43:39 +000063 @keyframes fadeInOut {
Autoformattercf570962025-04-30 17:27:39 +000064 0% {
65 opacity: 0;
66 }
67 20% {
68 opacity: 1;
69 }
70 80% {
71 opacity: 1;
72 }
73 100% {
74 opacity: 0;
75 }
Philip Zeyliger47b71c92025-04-30 15:43:39 +000076 }
Autoformattercf570962025-04-30 17:27:39 +000077
Philip Zeyliger47b71c92025-04-30 15:43:39 +000078 .commit-branch-indicator {
79 color: #28a745;
80 }
Autoformattercf570962025-04-30 17:27:39 +000081
Philip Zeyliger47b71c92025-04-30 15:43:39 +000082 .commit-hash-indicator {
83 color: #0366d6;
84 }
Sean McCullough86b56862025-04-18 13:04:03 -070085 :host {
86 display: block;
Sean McCullough71941bd2025-04-18 13:31:48 -070087 font-family:
88 system-ui,
89 -apple-system,
90 BlinkMacSystemFont,
91 "Segoe UI",
92 Roboto,
93 sans-serif;
Sean McCullough86b56862025-04-18 13:04:03 -070094 color: #333;
95 line-height: 1.4;
Pokey Rule4097e532025-04-24 18:55:28 +010096 height: 100vh;
Sean McCullough86b56862025-04-18 13:04:03 -070097 width: 100%;
98 position: relative;
99 overflow-x: hidden;
Pokey Rule4097e532025-04-24 18:55:28 +0100100 display: flex;
101 flex-direction: column;
Sean McCullough86b56862025-04-18 13:04:03 -0700102 }
103
104 /* Top banner with combined elements */
Pokey Rule4097e532025-04-24 18:55:28 +0100105 #top-banner {
Sean McCullough86b56862025-04-18 13:04:03 -0700106 display: flex;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000107 align-self: stretch;
Sean McCullough86b56862025-04-18 13:04:03 -0700108 justify-content: space-between;
109 align-items: center;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000110 padding: 0 20px;
Sean McCullough86b56862025-04-18 13:04:03 -0700111 margin-bottom: 0;
112 border-bottom: 1px solid #eee;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000113 gap: 20px;
Sean McCullough86b56862025-04-18 13:04:03 -0700114 background: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700115 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000116 width: 100%;
117 height: 48px;
118 padding-right: 30px; /* Extra padding on the right to prevent elements from hitting the edge */
Sean McCullough86b56862025-04-18 13:04:03 -0700119 }
120
Pokey Rule4097e532025-04-24 18:55:28 +0100121 /* View mode container styles - mirroring timeline.css structure */
122 #view-container {
123 align-self: stretch;
124 overflow-y: auto;
125 flex: 1;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700126 display: flex;
127 flex-direction: column;
128 min-height: 0; /* Critical for proper flex child behavior */
Pokey Rule4097e532025-04-24 18:55:28 +0100129 }
130
131 #view-container-inner {
132 max-width: 1200px;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700133 width: calc(100% - 40px);
Pokey Rule4097e532025-04-24 18:55:28 +0100134 margin: 0 auto;
135 position: relative;
136 padding-bottom: 10px;
137 padding-top: 10px;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700138 display: flex;
139 flex-direction: column;
140 height: 100%; /* Ensure it takes full height of parent */
Pokey Rule4097e532025-04-24 18:55:28 +0100141 }
142
143 #chat-input {
144 align-self: flex-end;
145 width: 100%;
146 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
147 }
148
Sean McCullough86b56862025-04-18 13:04:03 -0700149 .banner-title {
150 font-size: 18px;
151 font-weight: 600;
152 margin: 0;
153 min-width: 6em;
154 white-space: nowrap;
155 overflow: hidden;
156 text-overflow: ellipsis;
157 }
158
159 .chat-title {
160 margin: 0;
161 padding: 0;
162 color: rgba(82, 82, 82, 0.85);
Josh Bleecher Snydereb5166a2025-04-30 17:04:20 +0000163 font-size: 14px;
Sean McCullough86b56862025-04-18 13:04:03 -0700164 font-weight: normal;
165 font-style: italic;
166 white-space: nowrap;
167 overflow: hidden;
168 text-overflow: ellipsis;
169 }
170
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700171 /* Allow the container to expand to full width and height in diff mode */
172 #view-container-inner.diff-active,
173 #view-container-inner.diff2-active {
Sean McCullough86b56862025-04-18 13:04:03 -0700174 max-width: 100%;
Pokey Rule46fff972025-04-25 14:57:44 +0100175 width: 100%;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700176 height: 100%;
177 padding: 0; /* Remove padding for more space */
178 display: flex;
179 flex-direction: column;
180 flex: 1;
181 min-height: 0; /* Critical for flex behavior */
Sean McCullough86b56862025-04-18 13:04:03 -0700182 }
183
184 /* Individual view styles */
185 .chat-view,
186 .diff-view,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700187 .diff2-view,
Sean McCullough86b56862025-04-18 13:04:03 -0700188 .terminal-view {
189 display: none; /* Hidden by default */
190 width: 100%;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700191 height: 100%;
192 }
Autoformatter8c463622025-05-16 21:54:17 +0000193
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700194 /* Make chat view take full width available */
195 .chat-view.view-active {
196 display: flex;
197 flex-direction: column;
198 width: 100%;
199 }
Autoformatter8c463622025-05-16 21:54:17 +0000200
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700201 /* Monaco diff2 view needs to take all available space */
202 .diff2-view.view-active {
203 flex: 1;
204 overflow: hidden;
205 min-height: 0; /* Required for proper flex child behavior */
206 display: flex;
207 flex-direction: column;
208 height: 100%;
Sean McCullough86b56862025-04-18 13:04:03 -0700209 }
210
211 /* Active view styles - these will be applied via JavaScript */
212 .view-active {
213 display: flex;
214 flex-direction: column;
215 }
216
217 .title-container {
218 display: flex;
219 flex-direction: column;
220 white-space: nowrap;
221 overflow: hidden;
222 text-overflow: ellipsis;
Josh Bleecher Snydereb5166a2025-04-30 17:04:20 +0000223 max-width: 30%;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000224 padding: 6px 0;
Sean McCullough86b56862025-04-18 13:04:03 -0700225 }
226
227 .refresh-control {
228 display: flex;
229 align-items: center;
230 margin-bottom: 0;
231 flex-wrap: nowrap;
232 white-space: nowrap;
233 flex-shrink: 0;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000234 gap: 15px;
235 padding-left: 15px;
236 margin-right: 50px;
Sean McCullough86b56862025-04-18 13:04:03 -0700237 }
238
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000239 .restart-button,
Pokey Rule397871d2025-05-19 15:02:45 +0100240 .stop-button,
241 .end-button {
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700242 background: #2196f3;
243 color: white;
244 border: none;
245 padding: 4px 10px;
246 border-radius: 4px;
247 cursor: pointer;
248 font-size: 12px;
249 margin-right: 5px;
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000250 display: flex;
251 align-items: center;
252 gap: 6px;
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700253 }
254
255 .restart-button:hover {
256 background-color: #0b7dda;
257 }
258
259 .restart-button:disabled {
260 background-color: #ccc;
261 cursor: not-allowed;
262 opacity: 0.6;
263 }
264
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000265 .stop-button {
266 background: #dc3545;
Sean McCullough86b56862025-04-18 13:04:03 -0700267 color: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700268 }
269
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000270 .stop-button:hover:not(:disabled) {
271 background-color: #c82333;
Sean McCullough86b56862025-04-18 13:04:03 -0700272 }
273
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000274 .stop-button:disabled {
275 background-color: #e9a8ad;
276 cursor: not-allowed;
277 opacity: 0.7;
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000278 }
279
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000280 .stop-button:disabled:hover {
281 background-color: #e9a8ad;
282 }
283
Pokey Rule397871d2025-05-19 15:02:45 +0100284 .end-button {
285 background: #6c757d;
286 color: white;
287 }
288
289 .end-button:hover:not(:disabled) {
290 background-color: #5a6268;
291 }
292
293 .end-button:disabled {
294 background-color: #a9acaf;
295 cursor: not-allowed;
296 opacity: 0.7;
297 }
298
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000299 .button-icon {
300 width: 16px;
301 height: 16px;
302 }
303
304 @media (max-width: 1400px) {
305 .button-text {
306 display: none;
307 }
308
309 .restart-button,
310 .stop-button {
311 padding: 6px;
312 }
313 }
314
315 /* Removed poll-updates class */
316
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000317 .notifications-toggle {
Sean McCullough86b56862025-04-18 13:04:03 -0700318 display: flex;
319 align-items: center;
Sean McCullough86b56862025-04-18 13:04:03 -0700320 font-size: 12px;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000321 margin-right: 10px;
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000322 cursor: pointer;
323 }
324
325 .bell-icon {
326 width: 20px;
327 height: 20px;
328 position: relative;
329 display: inline-flex;
330 align-items: center;
331 justify-content: center;
332 }
333
334 .bell-disabled::before {
335 content: "";
336 position: absolute;
337 width: 2px;
338 height: 24px;
339 background-color: #dc3545;
340 transform: rotate(45deg);
341 transform-origin: center center;
Sean McCullough86b56862025-04-18 13:04:03 -0700342 }
343 `;
344
345 // Header bar: Network connection status details
346 @property()
347 connectionStatus: ConnectionStatus = "disconnected";
Autoformattercf570962025-04-30 17:27:39 +0000348
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000349 // Track if the last commit info has been copied
350 @state()
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000351 // lastCommitCopied moved to sketch-container-status
Sean McCullough86b56862025-04-18 13:04:03 -0700352
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000353 // Track notification preferences
354 @state()
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000355 notificationsEnabled: boolean = false;
356
357 // Track if the window is focused to control notifications
358 @state()
359 private _windowFocused: boolean = document.hasFocus();
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000360
Sean McCullough86b56862025-04-18 13:04:03 -0700361 @property()
362 connectionErrorMessage: string = "";
363
Sean McCullough86b56862025-04-18 13:04:03 -0700364 // Chat messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100365 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700366 messages: AgentMessage[] = [];
Sean McCullough86b56862025-04-18 13:04:03 -0700367
368 @property()
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000369 set title(value: string) {
370 const oldValue = this._title;
371 this._title = value;
372 this.requestUpdate("title", oldValue);
373 // Update document title when title property changes
374 this.updateDocumentTitle();
375 }
376
377 get title(): string {
378 return this._title;
379 }
380
381 private _title: string = "";
Sean McCullough86b56862025-04-18 13:04:03 -0700382
383 private dataManager = new DataManager();
384
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100385 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700386 containerState: State = {
Philip Zeyligerd03318d2025-05-08 13:09:12 -0700387 state_version: 2,
Sean McCulloughd9f13372025-04-21 15:08:49 -0700388 title: "",
389 os: "",
390 message_count: 0,
391 hostname: "",
392 working_dir: "",
393 initial_commit: "",
Philip Zeyliger99a9a022025-04-27 15:15:25 +0000394 outstanding_llm_calls: 0,
395 outstanding_tool_calls: [],
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000396 session_id: "",
397 ssh_available: false,
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700398 ssh_error: "",
399 in_container: false,
400 first_message_index: 0,
Sean McCulloughd9f13372025-04-21 15:08:49 -0700401 };
Sean McCullough86b56862025-04-18 13:04:03 -0700402
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700403 @state()
404 private restartModalOpen = false;
405
Sean McCullough86b56862025-04-18 13:04:03 -0700406 // Mutation observer to detect when new messages are added
407 private mutationObserver: MutationObserver | null = null;
408
409 constructor() {
410 super();
411
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000412 // Reference to the container status element
413 this.containerStatusElement = null;
414
Sean McCullough86b56862025-04-18 13:04:03 -0700415 // Binding methods to this
416 this._handleViewModeSelect = this._handleViewModeSelect.bind(this);
Sean McCullough34bb09a2025-05-13 15:39:54 -0700417 this._handlePopState = this._handlePopState.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -0700418 this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
Sean McCullough485afc62025-04-28 14:28:39 -0700419 this._handleMutlipleChoiceSelected =
420 this._handleMutlipleChoiceSelected.bind(this);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000421 this._handleStopClick = this._handleStopClick.bind(this);
Pokey Rule397871d2025-05-19 15:02:45 +0100422 this._handleEndClick = this._handleEndClick.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000423 this._handleNotificationsToggle =
424 this._handleNotificationsToggle.bind(this);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000425 this._handleWindowFocus = this._handleWindowFocus.bind(this);
426 this._handleWindowBlur = this._handleWindowBlur.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000427
428 // Load notification preference from localStorage
429 try {
430 const savedPref = localStorage.getItem("sketch-notifications-enabled");
431 if (savedPref !== null) {
432 this.notificationsEnabled = savedPref === "true";
433 }
434 } catch (error) {
435 console.error("Error loading notification preference:", error);
436 }
Sean McCullough86b56862025-04-18 13:04:03 -0700437 }
438
439 // See https://lit.dev/docs/components/lifecycle/
440 connectedCallback() {
441 super.connectedCallback();
442
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000443 // Get reference to the container status element
444 setTimeout(() => {
445 this.containerStatusElement =
446 this.shadowRoot?.getElementById("container-status");
447 }, 0);
448
Sean McCullough86b56862025-04-18 13:04:03 -0700449 // Initialize client-side nav history.
450 const url = new URL(window.location.href);
451 const mode = url.searchParams.get("view") || "chat";
452 window.history.replaceState({ mode }, "", url.toString());
453
454 this.toggleViewMode(mode as ViewMode, false);
455 // Add popstate event listener to handle browser back/forward navigation
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100456 window.addEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700457
458 // Add event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100459 window.addEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100460 window.addEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700461
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000462 // Add window focus/blur listeners for controlling notifications
463 window.addEventListener("focus", this._handleWindowFocus);
464 window.addEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700465 window.addEventListener(
466 "multiple-choice-selected",
467 this._handleMutlipleChoiceSelected,
468 );
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000469
Sean McCullough86b56862025-04-18 13:04:03 -0700470 // register event listeners
471 this.dataManager.addEventListener(
472 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700473 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700474 );
475 this.dataManager.addEventListener(
476 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700477 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700478 );
479
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000480 // Set initial document title
481 this.updateDocumentTitle();
482
Sean McCullough86b56862025-04-18 13:04:03 -0700483 // Initialize the data manager
484 this.dataManager.initialize();
Autoformattercf570962025-04-30 17:27:39 +0000485
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000486 // Process existing messages for commit info
487 if (this.messages && this.messages.length > 0) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000488 // Update last commit info via container status component
489 setTimeout(() => {
490 if (this.containerStatusElement) {
491 this.containerStatusElement.updateLastCommitInfo(this.messages);
492 }
493 }, 100);
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000494 }
Sean McCullough86b56862025-04-18 13:04:03 -0700495 }
496
497 // See https://lit.dev/docs/components/lifecycle/
498 disconnectedCallback() {
499 super.disconnectedCallback();
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100500 window.removeEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700501
502 // Remove event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100503 window.removeEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100504 window.removeEventListener("show-commit-diff", this._handleShowCommitDiff);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000505 window.removeEventListener("focus", this._handleWindowFocus);
506 window.removeEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700507 window.removeEventListener(
508 "multiple-choice-selected",
509 this._handleMutlipleChoiceSelected,
510 );
Sean McCullough86b56862025-04-18 13:04:03 -0700511
512 // unregister data manager event listeners
513 this.dataManager.removeEventListener(
514 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700515 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700516 );
517 this.dataManager.removeEventListener(
518 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700519 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700520 );
521
522 // Disconnect mutation observer if it exists
523 if (this.mutationObserver) {
Sean McCullough86b56862025-04-18 13:04:03 -0700524 this.mutationObserver.disconnect();
525 this.mutationObserver = null;
526 }
527 }
528
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700529 updateUrlForViewMode(mode: ViewMode): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700530 // Get the current URL without search parameters
531 const url = new URL(window.location.href);
532
533 // Clear existing parameters
534 url.search = "";
535
536 // Only add view parameter if not in default chat view
537 if (mode !== "chat") {
538 url.searchParams.set("view", mode);
Sean McCullough71941bd2025-04-18 13:31:48 -0700539 const diffView = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700540 ".diff-view",
Sean McCullough71941bd2025-04-18 13:31:48 -0700541 ) as SketchDiffView;
Sean McCullough86b56862025-04-18 13:04:03 -0700542
543 // If in diff view and there's a commit hash, include that too
544 if (mode === "diff" && diffView.commitHash) {
545 url.searchParams.set("commit", diffView.commitHash);
546 }
547 }
548
549 // Update the browser history without reloading the page
550 window.history.pushState({ mode }, "", url.toString());
551 }
552
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100553 private _handlePopState(event: PopStateEvent) {
Sean McCullough86b56862025-04-18 13:04:03 -0700554 if (event.state && event.state.mode) {
555 this.toggleViewMode(event.state.mode, false);
556 } else {
557 this.toggleViewMode("chat", false);
558 }
559 }
560
561 /**
562 * Handle view mode selection event
563 */
564 private _handleViewModeSelect(event: CustomEvent) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700565 const mode = event.detail.mode as "chat" | "diff" | "diff2" | "terminal";
Sean McCullough86b56862025-04-18 13:04:03 -0700566 this.toggleViewMode(mode, true);
567 }
568
569 /**
570 * Handle show commit diff event
571 */
572 private _handleShowCommitDiff(event: CustomEvent) {
573 const { commitHash } = event.detail;
574 if (commitHash) {
575 this.showCommitDiff(commitHash);
576 }
577 }
578
Sean McCullough485afc62025-04-28 14:28:39 -0700579 private _handleMultipleChoice(event: CustomEvent) {
580 window.console.log("_handleMultipleChoice", event);
581 this._sendChat;
582 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700583
584 private _handleDiffComment(event: CustomEvent) {
585 // Empty stub required by the event binding in the template
586 // Actual handling occurs at global level in sketch-chat-input component
587 }
Sean McCullough86b56862025-04-18 13:04:03 -0700588 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700589 * Listen for commit diff event
590 * @param commitHash The commit hash to show diff for
591 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100592 private showCommitDiff(commitHash: string): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700593 // Store the commit hash
594 this.currentCommitHash = commitHash;
595
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700596 this.toggleViewMode("diff2", true);
Sean McCullough86b56862025-04-18 13:04:03 -0700597
Sean McCullough86b56862025-04-18 13:04:03 -0700598 this.updateComplete.then(() => {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700599 const diff2View = this.shadowRoot?.querySelector("sketch-diff2-view");
600 if (diff2View) {
601 (diff2View as SketchDiff2View).refreshDiffView();
Sean McCullough86b56862025-04-18 13:04:03 -0700602 }
603 });
604 }
605
606 /**
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000607 * Toggle between different view modes: chat, diff, terminal
Sean McCullough86b56862025-04-18 13:04:03 -0700608 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100609 private toggleViewMode(mode: ViewMode, updateHistory: boolean): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700610 // Don't do anything if the mode is already active
611 if (this.viewMode === mode) return;
612
613 // Update the view mode
614 this.viewMode = mode;
615
616 if (updateHistory) {
617 // Update URL with the current view mode
618 this.updateUrlForViewMode(mode);
619 }
620
621 // Wait for DOM update to complete
622 this.updateComplete.then(() => {
623 // Update active view
Pokey Rule46fff972025-04-25 14:57:44 +0100624 const viewContainerInner = this.shadowRoot?.querySelector(
625 "#view-container-inner",
626 );
Sean McCullough86b56862025-04-18 13:04:03 -0700627 const chatView = this.shadowRoot?.querySelector(".chat-view");
628 const diffView = this.shadowRoot?.querySelector(".diff-view");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700629 const diff2View = this.shadowRoot?.querySelector(".diff2-view");
Sean McCullough86b56862025-04-18 13:04:03 -0700630 const terminalView = this.shadowRoot?.querySelector(".terminal-view");
631
632 // Remove active class from all views
633 chatView?.classList.remove("view-active");
634 diffView?.classList.remove("view-active");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700635 diff2View?.classList.remove("view-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700636 terminalView?.classList.remove("view-active");
637
638 // Add/remove diff-active class on view container
639 if (mode === "diff") {
Pokey Rule46fff972025-04-25 14:57:44 +0100640 viewContainerInner?.classList.add("diff-active");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700641 viewContainerInner?.classList.remove("diff2-active");
642 } else if (mode === "diff2") {
643 viewContainerInner?.classList.add("diff2-active");
644 viewContainerInner?.classList.remove("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700645 } else {
Pokey Rule46fff972025-04-25 14:57:44 +0100646 viewContainerInner?.classList.remove("diff-active");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700647 viewContainerInner?.classList.remove("diff2-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700648 }
649
650 // Add active class to the selected view
651 switch (mode) {
652 case "chat":
653 chatView?.classList.add("view-active");
654 break;
655 case "diff":
656 diffView?.classList.add("view-active");
657 // Load diff content if we have a diff view
658 const diffViewComp =
659 this.shadowRoot?.querySelector("sketch-diff-view");
660 if (diffViewComp && this.currentCommitHash) {
661 (diffViewComp as any).showCommitDiff(this.currentCommitHash);
662 } else if (diffViewComp) {
663 (diffViewComp as any).loadDiffContent();
664 }
665 break;
Autoformatter8c463622025-05-16 21:54:17 +0000666
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700667 case "diff2":
668 diff2View?.classList.add("view-active");
669 // Refresh git/recentlog when Monaco diff view is opened
670 // This ensures branch information is always up-to-date, as branches can change frequently
Autoformatter8c463622025-05-16 21:54:17 +0000671 const diff2ViewComp =
672 this.shadowRoot?.querySelector("sketch-diff2-view");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700673 if (diff2ViewComp) {
674 (diff2ViewComp as SketchDiff2View).refreshDiffView();
675 }
676 break;
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000677
Sean McCullough86b56862025-04-18 13:04:03 -0700678 case "terminal":
679 terminalView?.classList.add("view-active");
680 break;
681 }
682
683 // Update view mode buttons
684 const viewModeSelect = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700685 "sketch-view-mode-select",
Sean McCullough86b56862025-04-18 13:04:03 -0700686 );
687 if (viewModeSelect) {
688 const event = new CustomEvent("update-active-mode", {
689 detail: { mode },
690 bubbles: true,
691 composed: true,
692 });
693 viewModeSelect.dispatchEvent(event);
694 }
Sean McCullough86b56862025-04-18 13:04:03 -0700695 });
696 }
697
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000698 /**
699 * Updates the document title based on current title and connection status
700 */
701 private updateDocumentTitle(): void {
702 let docTitle = `sk: ${this.title || "untitled"}`;
703
704 // Add red circle emoji if disconnected
705 if (this.connectionStatus === "disconnected") {
706 docTitle += " 🔴";
707 }
708
709 document.title = docTitle;
710 }
711
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000712 // Check and request notification permission if needed
713 private async checkNotificationPermission(): Promise<boolean> {
714 // Check if the Notification API is supported
715 if (!("Notification" in window)) {
716 console.log("This browser does not support notifications");
717 return false;
718 }
719
720 // Check if permission is already granted
721 if (Notification.permission === "granted") {
722 return true;
723 }
724
725 // If permission is not denied, request it
726 if (Notification.permission !== "denied") {
727 const permission = await Notification.requestPermission();
728 return permission === "granted";
729 }
730
731 return false;
732 }
733
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000734 // Handle notifications toggle click
735 private _handleNotificationsToggle(): void {
736 this.notificationsEnabled = !this.notificationsEnabled;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000737
738 // If enabling notifications, check permissions
739 if (this.notificationsEnabled) {
740 this.checkNotificationPermission();
741 }
742
743 // Save preference to localStorage
744 try {
745 localStorage.setItem(
746 "sketch-notifications-enabled",
747 String(this.notificationsEnabled),
748 );
749 } catch (error) {
750 console.error("Error saving notification preference:", error);
751 }
752 }
753
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000754 // Handle window focus event
755 private _handleWindowFocus(): void {
756 this._windowFocused = true;
757 }
758
759 // Handle window blur event
760 private _handleWindowBlur(): void {
761 this._windowFocused = false;
762 }
763
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000764 // Show notification for message with EndOfTurn=true
765 private async showEndOfTurnNotification(
766 message: AgentMessage,
767 ): Promise<void> {
768 // Don't show notifications if they're disabled
769 if (!this.notificationsEnabled) return;
770
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000771 // Don't show notifications if the window is focused
772 if (this._windowFocused) return;
773
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000774 // Check if we have permission to show notifications
775 const hasPermission = await this.checkNotificationPermission();
776 if (!hasPermission) return;
777
Philip Zeyliger32011332025-04-30 20:59:40 +0000778 // Only show notifications for agent messages with end_of_turn=true and no parent_conversation_id
779 if (
780 message.type !== "agent" ||
781 !message.end_of_turn ||
782 message.parent_conversation_id
783 )
784 return;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000785
786 // Create a title that includes the sketch title
787 const notificationTitle = `Sketch: ${this.title || "untitled"}`;
788
789 // Extract the beginning of the message content (first 100 chars)
790 const messagePreview = message.content
791 ? message.content.substring(0, 100) +
792 (message.content.length > 100 ? "..." : "")
793 : "Agent has completed its turn";
794
795 // Create and show the notification
796 try {
797 new Notification(notificationTitle, {
798 body: messagePreview,
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000799 icon: "https://sketch.dev/favicon.ico", // Use sketch.dev favicon for notification
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000800 });
801 } catch (error) {
802 console.error("Error showing notification:", error);
803 }
804 }
805
Sean McCullough86b56862025-04-18 13:04:03 -0700806 private handleDataChanged(eventData: {
807 state: State;
Sean McCulloughd9f13372025-04-21 15:08:49 -0700808 newMessages: AgentMessage[];
Sean McCullough86b56862025-04-18 13:04:03 -0700809 }): void {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000810 const { state, newMessages } = eventData;
Sean McCullough86b56862025-04-18 13:04:03 -0700811
812 // Update state if we received it
813 if (state) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000814 // Ensure we're using the latest call status to prevent indicators from being stuck
Autoformatterf830c9d2025-04-30 18:16:01 +0000815 if (
816 state.outstanding_llm_calls === 0 &&
817 state.outstanding_tool_calls.length === 0
818 ) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000819 // Force reset containerState calls when nothing is reported as in progress
820 state.outstanding_llm_calls = 0;
821 state.outstanding_tool_calls = [];
822 }
Autoformatterf830c9d2025-04-30 18:16:01 +0000823
Sean McCullough86b56862025-04-18 13:04:03 -0700824 this.containerState = state;
825 this.title = state.title;
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000826
827 // Update document title when sketch title changes
828 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700829 }
830
Sean McCullough86b56862025-04-18 13:04:03 -0700831 // Update messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100832 this.messages = aggregateAgentMessages(this.messages, newMessages);
Autoformattercf570962025-04-30 17:27:39 +0000833
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000834 // Process new messages to find commit messages
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000835 // Update last commit info via container status component
836 if (this.containerStatusElement) {
837 this.containerStatusElement.updateLastCommitInfo(newMessages);
838 }
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000839
840 // Check for agent messages with end_of_turn=true and show notifications
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000841 if (newMessages && newMessages.length > 0) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000842 for (const message of newMessages) {
Philip Zeyliger32011332025-04-30 20:59:40 +0000843 if (
844 message.type === "agent" &&
845 message.end_of_turn &&
846 !message.parent_conversation_id
847 ) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000848 this.showEndOfTurnNotification(message);
849 break; // Only show one notification per batch of messages
850 }
851 }
852 }
Sean McCullough86b56862025-04-18 13:04:03 -0700853 }
854
855 private handleConnectionStatusChanged(
856 status: ConnectionStatus,
Philip Zeyliger72682df2025-04-23 13:09:46 -0700857 errorMessage?: string,
Sean McCullough86b56862025-04-18 13:04:03 -0700858 ): void {
859 this.connectionStatus = status;
860 this.connectionErrorMessage = errorMessage || "";
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000861
862 // Update document title when connection status changes
863 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700864 }
865
Sean McCulloughd3906e22025-04-29 17:32:14 +0000866 private async _handleStopClick(): Promise<void> {
867 try {
868 const response = await fetch("cancel", {
869 method: "POST",
870 headers: {
871 "Content-Type": "application/json",
872 },
873 body: JSON.stringify({ reason: "user requested cancellation" }),
874 });
875
876 if (!response.ok) {
877 const errorData = await response.text();
878 throw new Error(
879 `Failed to stop operation: ${response.status} - ${errorData}`,
880 );
881 }
882
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000883 // Stop request sent
Sean McCulloughd3906e22025-04-29 17:32:14 +0000884 } catch (error) {
885 console.error("Error stopping operation:", error);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000886 }
887 }
888
Pokey Rule397871d2025-05-19 15:02:45 +0100889 private async _handleEndClick(event?: Event): Promise<void> {
890 if (event) {
891 event.preventDefault();
892 event.stopPropagation();
893 }
894 // Show confirmation dialog
895 const confirmed = window.confirm(
896 "Ending the session will shut down the underlying container. Are you sure?",
897 );
898 if (!confirmed) return;
899
900 try {
901 const response = await fetch("end", {
902 method: "POST",
903 headers: {
904 "Content-Type": "application/json",
905 },
906 body: JSON.stringify({ reason: "user requested end of session" }),
907 });
908
909 if (!response.ok) {
910 const errorData = await response.text();
911 throw new Error(
912 `Failed to end session: ${response.status} - ${errorData}`,
913 );
914 }
915
916 // After successful response, redirect to messages view
917 // Extract the session ID from the URL
918 const currentUrl = window.location.href;
919 // The URL pattern should be like https://sketch.dev/s/cs71-8qa6-1124-aw79/
920 const urlParts = currentUrl.split("/");
921 let sessionId = "";
922
923 // Find the session ID in the URL (should be after /s/)
924 for (let i = 0; i < urlParts.length; i++) {
925 if (urlParts[i] === "s" && i + 1 < urlParts.length) {
926 sessionId = urlParts[i + 1];
927 break;
928 }
929 }
930
931 if (sessionId) {
932 // Create the messages URL
933 const messagesUrl = `/messages/${sessionId}`;
934 // Redirect to messages view
935 window.location.href = messagesUrl;
936 }
937
938 // End request sent - connection will be closed by server
939 } catch (error) {
940 console.error("Error ending session:", error);
941 }
942 }
943
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700944 openRestartModal() {
945 this.restartModalOpen = true;
946 }
947
948 handleRestartModalClose() {
949 this.restartModalOpen = false;
950 }
951
Sean McCullough485afc62025-04-28 14:28:39 -0700952 async _handleMutlipleChoiceSelected(e: CustomEvent) {
953 const chatInput = this.shadowRoot?.querySelector(
954 "sketch-chat-input",
955 ) as SketchChatInput;
956 if (chatInput) {
957 chatInput.content = e.detail.responseText;
958 chatInput.focus();
959 }
960 }
961
Sean McCullough86b56862025-04-18 13:04:03 -0700962 async _sendChat(e: CustomEvent) {
963 console.log("app shell: _sendChat", e);
Sean McCullough485afc62025-04-28 14:28:39 -0700964 e.preventDefault();
965 e.stopPropagation();
Sean McCullough86b56862025-04-18 13:04:03 -0700966 const message = e.detail.message?.trim();
967 if (message == "") {
968 return;
969 }
970 try {
Josh Bleecher Snyder98b64d12025-05-12 19:42:43 +0000971 // Always switch to chat view when sending a message so user can see processing
972 if (this.viewMode !== "chat") {
973 this.toggleViewMode("chat", true);
974 }
Autoformatter5c7f9572025-05-13 01:17:31 +0000975
Sean McCullough86b56862025-04-18 13:04:03 -0700976 // Send the message to the server
977 const response = await fetch("chat", {
978 method: "POST",
979 headers: {
980 "Content-Type": "application/json",
981 },
982 body: JSON.stringify({ message }),
983 });
984
985 if (!response.ok) {
986 const errorData = await response.text();
987 throw new Error(`Server error: ${response.status} - ${errorData}`);
988 }
Sean McCullough86b56862025-04-18 13:04:03 -0700989 } catch (error) {
990 console.error("Error sending chat message:", error);
991 const statusText = document.getElementById("statusText");
992 if (statusText) {
993 statusText.textContent = "Error sending message";
994 }
995 }
996 }
997
Pokey Rule4097e532025-04-24 18:55:28 +0100998 private scrollContainerRef = createRef<HTMLElement>();
999
Sean McCullough86b56862025-04-18 13:04:03 -07001000 render() {
1001 return html`
Pokey Rule4097e532025-04-24 18:55:28 +01001002 <div id="top-banner">
Sean McCullough86b56862025-04-18 13:04:03 -07001003 <div class="title-container">
1004 <h1 class="banner-title">sketch</h1>
1005 <h2 id="chatTitle" class="chat-title">${this.title}</h2>
1006 </div>
1007
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001008 <!-- Container status info moved above tabs -->
Sean McCullough86b56862025-04-18 13:04:03 -07001009 <sketch-container-status
1010 .state=${this.containerState}
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001011 id="container-status"
Sean McCullough86b56862025-04-18 13:04:03 -07001012 ></sketch-container-status>
Autoformattercf570962025-04-30 17:27:39 +00001013
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001014 <!-- Last Commit section moved to sketch-container-status -->
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001015
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001016 <!-- Views section with tabs -->
1017 <sketch-view-mode-select></sketch-view-mode-select>
Sean McCullough86b56862025-04-18 13:04:03 -07001018
1019 <div class="refresh-control">
Sean McCulloughd3906e22025-04-29 17:32:14 +00001020 <button
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001021 id="restartButton"
1022 class="restart-button"
1023 ?disabled=${this.containerState.message_count === 0}
1024 @click=${this.openRestartModal}
Sean McCulloughd3906e22025-04-29 17:32:14 +00001025 >
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001026 <svg
1027 class="button-icon"
1028 xmlns="http://www.w3.org/2000/svg"
1029 viewBox="0 0 24 24"
1030 fill="none"
1031 stroke="currentColor"
1032 stroke-width="2"
1033 stroke-linecap="round"
1034 stroke-linejoin="round"
1035 >
1036 <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
1037 <path d="M3 3v5h5" />
1038 </svg>
1039 <span class="button-text">Restart</span>
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001040 </button>
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001041 <button
1042 id="stopButton"
1043 class="stop-button"
1044 ?disabled=${(this.containerState?.outstanding_llm_calls || 0) ===
1045 0 &&
1046 (this.containerState?.outstanding_tool_calls || []).length === 0}
1047 >
1048 <svg
1049 class="button-icon"
1050 xmlns="http://www.w3.org/2000/svg"
1051 viewBox="0 0 24 24"
1052 fill="none"
1053 stroke="currentColor"
1054 stroke-width="2"
1055 stroke-linecap="round"
1056 stroke-linejoin="round"
1057 >
1058 <rect x="6" y="6" width="12" height="12" />
1059 </svg>
1060 <span class="button-text">Stop</span>
Sean McCullough86b56862025-04-18 13:04:03 -07001061 </button>
Pokey Rule397871d2025-05-19 15:02:45 +01001062 <button
1063 id="endButton"
1064 class="end-button"
1065 @click=${this._handleEndClick}
1066 >
1067 <svg
1068 class="button-icon"
1069 xmlns="http://www.w3.org/2000/svg"
1070 viewBox="0 0 24 24"
1071 fill="none"
1072 stroke="currentColor"
1073 stroke-width="2"
1074 stroke-linecap="round"
1075 stroke-linejoin="round"
1076 >
1077 <path d="M18 6L6 18" />
1078 <path d="M6 6l12 12" />
1079 </svg>
1080 <span class="button-text">End</span>
1081 </button>
Sean McCullough86b56862025-04-18 13:04:03 -07001082
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +00001083 <div
1084 class="notifications-toggle"
1085 @click=${this._handleNotificationsToggle}
1086 title="${this.notificationsEnabled
1087 ? "Disable"
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001088 : "Enable"} notifications when the agent completes its turn"
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +00001089 >
1090 <div
1091 class="bell-icon ${!this.notificationsEnabled
1092 ? "bell-disabled"
1093 : ""}"
1094 >
1095 <!-- Bell SVG icon -->
1096 <svg
1097 xmlns="http://www.w3.org/2000/svg"
1098 width="16"
1099 height="16"
1100 fill="currentColor"
1101 viewBox="0 0 16 16"
1102 >
1103 <path
1104 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"
1105 />
1106 </svg>
1107 </div>
Philip Zeyligerbc6b6292025-04-30 18:00:15 +00001108 </div>
1109
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001110 <sketch-call-status
Sean McCulloughd9d45812025-04-30 16:53:41 -07001111 .agentState=${this.containerState?.agent_state}
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001112 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
1113 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
Philip Zeyliger72318392025-05-14 02:56:07 +00001114 .isIdle=${this.messages.length > 0
1115 ? this.messages[this.messages.length - 1]?.end_of_turn &&
1116 !this.messages[this.messages.length - 1]?.parent_conversation_id
1117 : true}
Philip Zeyliger5e357022025-05-16 04:50:34 +00001118 .isDisconnected=${this.connectionStatus === "disconnected"}
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001119 ></sketch-call-status>
Philip Zeyliger25f6ff12025-05-02 04:24:10 +00001120
1121 <sketch-network-status
1122 connection=${this.connectionStatus}
1123 error=${this.connectionErrorMessage}
1124 ></sketch-network-status>
Sean McCullough86b56862025-04-18 13:04:03 -07001125 </div>
1126 </div>
1127
Pokey Rule4097e532025-04-24 18:55:28 +01001128 <div id="view-container" ${ref(this.scrollContainerRef)}>
1129 <div id="view-container-inner">
1130 <div
1131 class="chat-view ${this.viewMode === "chat" ? "view-active" : ""}"
1132 >
1133 <sketch-timeline
1134 .messages=${this.messages}
1135 .scrollContainer=${this.scrollContainerRef}
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001136 .agentState=${this.containerState?.agent_state}
1137 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
1138 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
Pokey Rule4097e532025-04-24 18:55:28 +01001139 ></sketch-timeline>
1140 </div>
1141 <div
1142 class="diff-view ${this.viewMode === "diff" ? "view-active" : ""}"
1143 >
1144 <sketch-diff-view
1145 .commitHash=${this.currentCommitHash}
1146 ></sketch-diff-view>
1147 </div>
Autoformatter8c463622025-05-16 21:54:17 +00001148
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001149 <div
1150 class="diff2-view ${this.viewMode === "diff2" ? "view-active" : ""}"
1151 >
1152 <sketch-diff2-view
1153 .commit=${this.currentCommitHash}
1154 .gitService=${new DefaultGitDataService()}
1155 @diff-comment="${this._handleDiffComment}"
1156 ></sketch-diff2-view>
1157 </div>
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +00001158
Pokey Rule4097e532025-04-24 18:55:28 +01001159 <div
1160 class="terminal-view ${this.viewMode === "terminal"
1161 ? "view-active"
1162 : ""}"
1163 >
1164 <sketch-terminal></sketch-terminal>
1165 </div>
Sean McCullough86b56862025-04-18 13:04:03 -07001166 </div>
1167 </div>
1168
Pokey Rule4097e532025-04-24 18:55:28 +01001169 <div id="chat-input">
1170 <sketch-chat-input @send-chat="${this._sendChat}"></sketch-chat-input>
1171 </div>
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001172
1173 <sketch-restart-modal
1174 ?open=${this.restartModalOpen}
1175 @close=${this.handleRestartModalClose}
1176 .containerState=${this.containerState}
1177 .messages=${this.messages}
1178 ></sketch-restart-modal>
Sean McCullough86b56862025-04-18 13:04:03 -07001179 `;
1180 }
1181
1182 /**
Sean McCullough86b56862025-04-18 13:04:03 -07001183 * Lifecycle callback when component is first connected to DOM
1184 */
1185 firstUpdated(): void {
1186 if (this.viewMode !== "chat") {
1187 return;
1188 }
1189
1190 // Initial scroll to bottom when component is first rendered
1191 setTimeout(
1192 () => this.scrollTo({ top: this.scrollHeight, behavior: "smooth" }),
Philip Zeyliger72682df2025-04-23 13:09:46 -07001193 50,
Sean McCullough86b56862025-04-18 13:04:03 -07001194 );
1195
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001196 // Setup stop button
1197 const stopButton = this.renderRoot?.querySelector(
1198 "#stopButton",
1199 ) as HTMLButtonElement;
1200 stopButton?.addEventListener("click", async () => {
1201 try {
Sean McCullough495cb962025-05-01 16:25:53 -07001202 const response = await fetch("cancel", {
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001203 method: "POST",
1204 headers: {
1205 "Content-Type": "application/json",
1206 },
1207 body: JSON.stringify({ reason: "User clicked stop button" }),
1208 });
1209 if (!response.ok) {
1210 console.error("Failed to cancel:", await response.text());
1211 }
1212 } catch (error) {
1213 console.error("Error cancelling operation:", error);
1214 }
1215 });
1216
Pokey Rule397871d2025-05-19 15:02:45 +01001217 // Setup end button
1218 const endButton = this.renderRoot?.querySelector(
1219 "#endButton",
1220 ) as HTMLButtonElement;
1221 // We're already using the @click binding in the HTML, so manual event listener not needed here
1222
Philip Zeyliger47b71c92025-04-30 15:43:39 +00001223 // Process any existing messages to find commit information
1224 if (this.messages && this.messages.length > 0) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001225 // Update last commit info via container status component
1226 if (this.containerStatusElement) {
1227 this.containerStatusElement.updateLastCommitInfo(this.messages);
1228 }
Philip Zeyliger47b71c92025-04-30 15:43:39 +00001229 }
Sean McCullough86b56862025-04-18 13:04:03 -07001230 }
1231}
1232
1233declare global {
1234 interface HTMLElementTagNameMap {
1235 "sketch-app-shell": SketchAppShell;
1236 }
1237}