blob: e2d27a280427c46eb0e29b5ab48eb3fb419f5fb6 [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";
Philip Zeyliger00bcaef2025-05-30 04:21:15 +00009
Philip Zeyliger272a90e2025-05-16 14:49:51 -070010import "./sketch-diff2-view";
11import { SketchDiff2View } from "./sketch-diff2-view";
12import { DefaultGitDataService } from "./git-data-service";
13import "./sketch-monaco-view";
Pokey Rule4097e532025-04-24 18:55:28 +010014import "./sketch-network-status";
Philip Zeyliger99a9a022025-04-27 15:15:25 +000015import "./sketch-call-status";
Pokey Rule4097e532025-04-24 18:55:28 +010016import "./sketch-terminal";
17import "./sketch-timeline";
18import "./sketch-view-mode-select";
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070019import "./sketch-todo-panel";
Pokey Rule4097e532025-04-24 18:55:28 +010020
21import { createRef, ref } from "lit/directives/ref.js";
Sean McCullough485afc62025-04-28 14:28:39 -070022import { SketchChatInput } from "./sketch-chat-input";
Sean McCullough86b56862025-04-18 13:04:03 -070023
Philip Zeyliger00bcaef2025-05-30 04:21:15 +000024type ViewMode = "chat" | "diff2" | "terminal";
Sean McCullough86b56862025-04-18 13:04:03 -070025
26@customElement("sketch-app-shell")
27export class SketchAppShell extends LitElement {
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +000028 // Current view mode (chat, diff, terminal)
Sean McCullough86b56862025-04-18 13:04:03 -070029 @state()
Philip Zeyliger272a90e2025-05-16 14:49:51 -070030 viewMode: ViewMode = "chat";
Sean McCullough86b56862025-04-18 13:04:03 -070031
32 // Current commit hash for diff view
33 @state()
34 currentCommitHash: string = "";
35
Philip Zeyliger47b71c92025-04-30 15:43:39 +000036 // Last commit information
37 @state()
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000038
39 // Reference to the container status element
40 containerStatusElement: any = null;
Philip Zeyliger47b71c92025-04-30 15:43:39 +000041
Sean McCullough86b56862025-04-18 13:04:03 -070042 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
43 // Note that these styles only apply to the scope of this web component's
44 // shadow DOM node, so they won't leak out or collide with CSS declared in
45 // other components or the containing web page (...unless you want it to do that).
46 static styles = css`
Philip Zeyliger47b71c92025-04-30 15:43:39 +000047 .copied-indicator {
48 position: absolute;
49 top: -20px;
50 left: 50%;
51 transform: translateX(-50%);
52 background: rgba(40, 167, 69, 0.9);
53 color: white;
54 padding: 2px 6px;
55 border-radius: 3px;
56 font-size: 10px;
57 font-family: system-ui, sans-serif;
58 animation: fadeInOut 2s ease;
59 pointer-events: none;
60 }
Autoformattercf570962025-04-30 17:27:39 +000061
Philip Zeyliger47b71c92025-04-30 15:43:39 +000062 @keyframes fadeInOut {
Autoformattercf570962025-04-30 17:27:39 +000063 0% {
64 opacity: 0;
65 }
66 20% {
67 opacity: 1;
68 }
69 80% {
70 opacity: 1;
71 }
72 100% {
73 opacity: 0;
74 }
Philip Zeyliger47b71c92025-04-30 15:43:39 +000075 }
Autoformattercf570962025-04-30 17:27:39 +000076
Philip Zeyliger47b71c92025-04-30 15:43:39 +000077 .commit-branch-indicator {
78 color: #28a745;
79 }
Autoformattercf570962025-04-30 17:27:39 +000080
Philip Zeyliger47b71c92025-04-30 15:43:39 +000081 .commit-hash-indicator {
82 color: #0366d6;
83 }
Sean McCullough86b56862025-04-18 13:04:03 -070084 :host {
85 display: block;
Sean McCullough71941bd2025-04-18 13:31:48 -070086 font-family:
87 system-ui,
88 -apple-system,
89 BlinkMacSystemFont,
90 "Segoe UI",
91 Roboto,
92 sans-serif;
Sean McCullough86b56862025-04-18 13:04:03 -070093 color: #333;
94 line-height: 1.4;
Pokey Rule4097e532025-04-24 18:55:28 +010095 height: 100vh;
Sean McCullough86b56862025-04-18 13:04:03 -070096 width: 100%;
97 position: relative;
98 overflow-x: hidden;
Pokey Rule4097e532025-04-24 18:55:28 +010099 display: flex;
100 flex-direction: column;
Sean McCullough86b56862025-04-18 13:04:03 -0700101 }
102
103 /* Top banner with combined elements */
Pokey Rule4097e532025-04-24 18:55:28 +0100104 #top-banner {
Sean McCullough86b56862025-04-18 13:04:03 -0700105 display: flex;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000106 align-self: stretch;
Sean McCullough86b56862025-04-18 13:04:03 -0700107 justify-content: space-between;
108 align-items: center;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000109 padding: 0 20px;
Sean McCullough86b56862025-04-18 13:04:03 -0700110 margin-bottom: 0;
111 border-bottom: 1px solid #eee;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000112 gap: 20px;
Sean McCullough86b56862025-04-18 13:04:03 -0700113 background: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700114 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000115 width: 100%;
116 height: 48px;
117 padding-right: 30px; /* Extra padding on the right to prevent elements from hitting the edge */
Sean McCullough86b56862025-04-18 13:04:03 -0700118 }
119
Pokey Rule4097e532025-04-24 18:55:28 +0100120 /* View mode container styles - mirroring timeline.css structure */
121 #view-container {
122 align-self: stretch;
123 overflow-y: auto;
124 flex: 1;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700125 display: flex;
126 flex-direction: column;
127 min-height: 0; /* Critical for proper flex child behavior */
Pokey Rule4097e532025-04-24 18:55:28 +0100128 }
129
130 #view-container-inner {
131 max-width: 1200px;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700132 width: calc(100% - 40px);
Pokey Rule4097e532025-04-24 18:55:28 +0100133 margin: 0 auto;
134 position: relative;
135 padding-bottom: 10px;
136 padding-top: 10px;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700137 display: flex;
138 flex-direction: column;
139 height: 100%; /* Ensure it takes full height of parent */
Pokey Rule4097e532025-04-24 18:55:28 +0100140 }
141
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700142 /* Adjust view container when todo panel is visible in chat mode */
143 #view-container-inner.with-todo-panel {
144 max-width: none;
145 width: 100%;
146 margin: 0;
147 padding-left: 20px;
148 padding-right: 20px;
149 }
150
Pokey Rule4097e532025-04-24 18:55:28 +0100151 #chat-input {
152 align-self: flex-end;
153 width: 100%;
154 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
155 }
156
Sean McCullough86b56862025-04-18 13:04:03 -0700157 .banner-title {
158 font-size: 18px;
159 font-weight: 600;
160 margin: 0;
161 min-width: 6em;
162 white-space: nowrap;
163 overflow: hidden;
164 text-overflow: ellipsis;
165 }
166
Philip Zeyliger0113be52025-06-07 23:53:41 +0000167 .banner-title a {
168 color: inherit;
169 text-decoration: none;
170 transition: opacity 0.2s ease;
171 display: flex;
172 align-items: center;
173 gap: 8px;
174 }
175
176 .banner-title a:hover {
177 opacity: 0.8;
178 text-decoration: underline;
179 }
180
181 .banner-title img {
182 width: 20px;
183 height: 20px;
184 border-radius: 3px;
185 }
186
187 /* Mobile-specific styles for banner link */
188 @media (max-width: 768px) {
189 .title-container {
190 max-width: 50%; /* Allow more space on mobile */
191 }
192
193 .banner-title {
194 font-size: 16px; /* Slightly smaller on mobile */
195 }
196
197 .banner-title img {
198 width: 18px;
199 height: 18px;
200 }
201 }
202
203 @media (max-width: 480px) {
204 .title-container {
205 max-width: 60%; /* Even more space on very small screens */
206 }
207
208 .banner-title {
209 font-size: 14px;
210 }
211
212 .banner-title img {
213 width: 16px;
214 height: 16px;
215 }
216 }
217
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700218 .slug-title {
Sean McCullough86b56862025-04-18 13:04:03 -0700219 margin: 0;
220 padding: 0;
221 color: rgba(82, 82, 82, 0.85);
Josh Bleecher Snydereb5166a2025-04-30 17:04:20 +0000222 font-size: 14px;
Sean McCullough86b56862025-04-18 13:04:03 -0700223 font-weight: normal;
224 font-style: italic;
225 white-space: nowrap;
226 overflow: hidden;
227 text-overflow: ellipsis;
228 }
229
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700230 /* Allow the container to expand to full width and height in diff mode */
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700231 #view-container-inner.diff2-active {
Sean McCullough86b56862025-04-18 13:04:03 -0700232 max-width: 100%;
Pokey Rule46fff972025-04-25 14:57:44 +0100233 width: 100%;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700234 height: 100%;
235 padding: 0; /* Remove padding for more space */
236 display: flex;
237 flex-direction: column;
238 flex: 1;
239 min-height: 0; /* Critical for flex behavior */
Sean McCullough86b56862025-04-18 13:04:03 -0700240 }
241
242 /* Individual view styles */
243 .chat-view,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700244 .diff2-view,
Sean McCullough86b56862025-04-18 13:04:03 -0700245 .terminal-view {
246 display: none; /* Hidden by default */
247 width: 100%;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700248 height: 100%;
249 }
Autoformatter8c463622025-05-16 21:54:17 +0000250
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700251 /* Make chat view take full width available */
252 .chat-view.view-active {
253 display: flex;
254 flex-direction: column;
255 width: 100%;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700256 height: 100%;
257 }
258
259 /* Chat timeline container - takes full width, memory panel will be positioned separately */
260 .chat-timeline-container {
261 flex: 1;
262 display: flex;
263 flex-direction: column;
264 width: 100%;
265 height: 100%;
266 margin-right: 0; /* Default - no memory panel */
267 transition: margin-right 0.2s ease; /* Smooth transition */
268 }
269
270 /* Adjust chat timeline container when todo panel is visible */
271 .chat-timeline-container.with-todo-panel {
272 margin-right: 400px; /* Make space for fixed todo panel */
273 width: calc(100% - 400px); /* Explicitly set width to prevent overlap */
274 }
275
276 /* Todo panel container - fixed to right side */
277 .todo-panel-container {
278 position: fixed;
279 top: 48px; /* Below top banner */
280 right: 15px; /* Leave space for scroll bar */
281 width: 400px;
282 bottom: var(
283 --chat-input-height,
284 90px
285 ); /* Dynamic height based on chat input size */
286 background-color: #fafafa;
287 border-left: 1px solid #e0e0e0;
288 z-index: 100;
289 display: none; /* Hidden by default */
290 transition: bottom 0.2s ease; /* Smooth transition when height changes */
291 /* Add fuzzy gradient at bottom to blend with text entry */
292 background: linear-gradient(
293 to bottom,
294 #fafafa 0%,
295 #fafafa 90%,
296 rgba(250, 250, 250, 0.5) 95%,
297 rgba(250, 250, 250, 0.2) 100%
298 );
299 }
300
301 .todo-panel-container.visible {
302 display: block;
303 }
304
305 /* Responsive adjustments for todo panel */
306 @media (max-width: 1200px) {
307 .todo-panel-container {
308 width: 350px;
309 /* bottom is still controlled by --chat-input-height CSS variable */
310 }
311 .chat-timeline-container.with-todo-panel {
312 margin-right: 350px;
313 width: calc(100% - 350px);
314 }
315 }
316
317 @media (max-width: 900px) {
318 .todo-panel-container {
319 width: 300px;
320 /* bottom is still controlled by --chat-input-height CSS variable */
321 }
322 .chat-timeline-container.with-todo-panel {
323 margin-right: 300px;
324 width: calc(100% - 300px);
325 }
326 }
327
328 /* On very small screens, hide todo panel or make it overlay */
329 @media (max-width: 768px) {
330 .todo-panel-container.visible {
331 display: none; /* Hide on mobile */
332 }
333 .chat-timeline-container.with-todo-panel {
334 margin-right: 0;
335 width: 100%;
336 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700337 }
Autoformatter8c463622025-05-16 21:54:17 +0000338
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700339 /* Monaco diff2 view needs to take all available space */
340 .diff2-view.view-active {
341 flex: 1;
342 overflow: hidden;
343 min-height: 0; /* Required for proper flex child behavior */
344 display: flex;
345 flex-direction: column;
346 height: 100%;
Sean McCullough86b56862025-04-18 13:04:03 -0700347 }
348
349 /* Active view styles - these will be applied via JavaScript */
350 .view-active {
351 display: flex;
352 flex-direction: column;
353 }
354
355 .title-container {
356 display: flex;
357 flex-direction: column;
358 white-space: nowrap;
359 overflow: hidden;
360 text-overflow: ellipsis;
Josh Bleecher Snydereb5166a2025-04-30 17:04:20 +0000361 max-width: 30%;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000362 padding: 6px 0;
Sean McCullough86b56862025-04-18 13:04:03 -0700363 }
364
365 .refresh-control {
366 display: flex;
367 align-items: center;
368 margin-bottom: 0;
369 flex-wrap: nowrap;
370 white-space: nowrap;
371 flex-shrink: 0;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000372 gap: 15px;
373 padding-left: 15px;
374 margin-right: 50px;
Sean McCullough86b56862025-04-18 13:04:03 -0700375 }
376
Pokey Rule397871d2025-05-19 15:02:45 +0100377 .stop-button,
378 .end-button {
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700379 background: #2196f3;
380 color: white;
381 border: none;
382 padding: 4px 10px;
383 border-radius: 4px;
384 cursor: pointer;
385 font-size: 12px;
386 margin-right: 5px;
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000387 display: flex;
388 align-items: center;
389 gap: 6px;
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700390 }
391
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000392 .stop-button {
393 background: #dc3545;
Sean McCullough86b56862025-04-18 13:04:03 -0700394 color: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700395 }
396
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000397 .stop-button:hover:not(:disabled) {
398 background-color: #c82333;
Sean McCullough86b56862025-04-18 13:04:03 -0700399 }
400
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000401 .stop-button:disabled {
402 background-color: #e9a8ad;
403 cursor: not-allowed;
404 opacity: 0.7;
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000405 }
406
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000407 .stop-button:disabled:hover {
408 background-color: #e9a8ad;
409 }
410
Pokey Rule397871d2025-05-19 15:02:45 +0100411 .end-button {
412 background: #6c757d;
413 color: white;
414 }
415
416 .end-button:hover:not(:disabled) {
417 background-color: #5a6268;
418 }
419
420 .end-button:disabled {
421 background-color: #a9acaf;
422 cursor: not-allowed;
423 opacity: 0.7;
424 }
425
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000426 .button-icon {
427 width: 16px;
428 height: 16px;
429 }
430
431 @media (max-width: 1400px) {
432 .button-text {
433 display: none;
434 }
435
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000436 .stop-button {
437 padding: 6px;
438 }
439 }
440
441 /* Removed poll-updates class */
442
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000443 .notifications-toggle {
Sean McCullough86b56862025-04-18 13:04:03 -0700444 display: flex;
445 align-items: center;
Sean McCullough86b56862025-04-18 13:04:03 -0700446 font-size: 12px;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000447 margin-right: 10px;
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000448 cursor: pointer;
449 }
450
451 .bell-icon {
452 width: 20px;
453 height: 20px;
454 position: relative;
455 display: inline-flex;
456 align-items: center;
457 justify-content: center;
458 }
459
460 .bell-disabled::before {
461 content: "";
462 position: absolute;
463 width: 2px;
464 height: 24px;
465 background-color: #dc3545;
466 transform: rotate(45deg);
467 transform-origin: center center;
Sean McCullough86b56862025-04-18 13:04:03 -0700468 }
469 `;
470
471 // Header bar: Network connection status details
472 @property()
473 connectionStatus: ConnectionStatus = "disconnected";
Autoformattercf570962025-04-30 17:27:39 +0000474
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000475 // Track if the last commit info has been copied
476 @state()
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000477 // lastCommitCopied moved to sketch-container-status
Sean McCullough86b56862025-04-18 13:04:03 -0700478
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000479 // Track notification preferences
480 @state()
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000481 notificationsEnabled: boolean = false;
482
483 // Track if the window is focused to control notifications
484 @state()
485 private _windowFocused: boolean = document.hasFocus();
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000486
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700487 // Track if the todo panel should be visible
488 @state()
489 private _todoPanelVisible: boolean = false;
490
491 // ResizeObserver for tracking chat input height changes
492 private chatInputResizeObserver: ResizeObserver | null = null;
493
Sean McCullough86b56862025-04-18 13:04:03 -0700494 @property()
495 connectionErrorMessage: string = "";
496
Sean McCullough86b56862025-04-18 13:04:03 -0700497 // Chat messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100498 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700499 messages: AgentMessage[] = [];
Sean McCullough86b56862025-04-18 13:04:03 -0700500
501 @property()
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700502 set slug(value: string) {
503 const oldValue = this._slug;
504 this._slug = value;
505 this.requestUpdate("slug", oldValue);
506 // Update document title when slug property changes
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000507 this.updateDocumentTitle();
508 }
509
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700510 get slug(): string {
511 return this._slug;
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000512 }
513
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700514 private _slug: string = "";
Sean McCullough86b56862025-04-18 13:04:03 -0700515
516 private dataManager = new DataManager();
517
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100518 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700519 containerState: State = {
Philip Zeyligerd03318d2025-05-08 13:09:12 -0700520 state_version: 2,
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700521 slug: "",
Sean McCulloughd9f13372025-04-21 15:08:49 -0700522 os: "",
523 message_count: 0,
524 hostname: "",
525 working_dir: "",
526 initial_commit: "",
Philip Zeyliger99a9a022025-04-27 15:15:25 +0000527 outstanding_llm_calls: 0,
528 outstanding_tool_calls: [],
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000529 session_id: "",
530 ssh_available: false,
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700531 ssh_error: "",
532 in_container: false,
533 first_message_index: 0,
Philip Zeyliger64f60462025-06-16 13:57:10 -0700534 diff_lines_added: 0,
535 diff_lines_removed: 0,
Sean McCulloughd9f13372025-04-21 15:08:49 -0700536 };
Sean McCullough86b56862025-04-18 13:04:03 -0700537
Sean McCullough86b56862025-04-18 13:04:03 -0700538 // Mutation observer to detect when new messages are added
539 private mutationObserver: MutationObserver | null = null;
540
541 constructor() {
542 super();
543
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000544 // Reference to the container status element
545 this.containerStatusElement = null;
546
Sean McCullough86b56862025-04-18 13:04:03 -0700547 // Binding methods to this
548 this._handleViewModeSelect = this._handleViewModeSelect.bind(this);
Sean McCullough34bb09a2025-05-13 15:39:54 -0700549 this._handlePopState = this._handlePopState.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -0700550 this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
Sean McCullough485afc62025-04-28 14:28:39 -0700551 this._handleMutlipleChoiceSelected =
552 this._handleMutlipleChoiceSelected.bind(this);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000553 this._handleStopClick = this._handleStopClick.bind(this);
Pokey Rule397871d2025-05-19 15:02:45 +0100554 this._handleEndClick = this._handleEndClick.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000555 this._handleNotificationsToggle =
556 this._handleNotificationsToggle.bind(this);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000557 this._handleWindowFocus = this._handleWindowFocus.bind(this);
558 this._handleWindowBlur = this._handleWindowBlur.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000559
560 // Load notification preference from localStorage
561 try {
562 const savedPref = localStorage.getItem("sketch-notifications-enabled");
563 if (savedPref !== null) {
564 this.notificationsEnabled = savedPref === "true";
565 }
566 } catch (error) {
567 console.error("Error loading notification preference:", error);
568 }
Sean McCullough86b56862025-04-18 13:04:03 -0700569 }
570
571 // See https://lit.dev/docs/components/lifecycle/
572 connectedCallback() {
573 super.connectedCallback();
574
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000575 // Get reference to the container status element
576 setTimeout(() => {
577 this.containerStatusElement =
578 this.shadowRoot?.getElementById("container-status");
579 }, 0);
580
Sean McCullough86b56862025-04-18 13:04:03 -0700581 // Initialize client-side nav history.
582 const url = new URL(window.location.href);
583 const mode = url.searchParams.get("view") || "chat";
584 window.history.replaceState({ mode }, "", url.toString());
585
586 this.toggleViewMode(mode as ViewMode, false);
587 // Add popstate event listener to handle browser back/forward navigation
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100588 window.addEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700589
590 // Add event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100591 window.addEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100592 window.addEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700593
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000594 // Add window focus/blur listeners for controlling notifications
595 window.addEventListener("focus", this._handleWindowFocus);
596 window.addEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700597 window.addEventListener(
598 "multiple-choice-selected",
599 this._handleMutlipleChoiceSelected,
600 );
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000601
Sean McCullough86b56862025-04-18 13:04:03 -0700602 // register event listeners
603 this.dataManager.addEventListener(
604 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700605 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700606 );
607 this.dataManager.addEventListener(
608 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700609 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700610 );
611
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000612 // Set initial document title
613 this.updateDocumentTitle();
614
Sean McCullough86b56862025-04-18 13:04:03 -0700615 // Initialize the data manager
616 this.dataManager.initialize();
Autoformattercf570962025-04-30 17:27:39 +0000617
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000618 // Process existing messages for commit info
619 if (this.messages && this.messages.length > 0) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000620 // Update last commit info via container status component
621 setTimeout(() => {
622 if (this.containerStatusElement) {
623 this.containerStatusElement.updateLastCommitInfo(this.messages);
624 }
625 }, 100);
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000626 }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700627
628 // Check if todo panel should be visible on initial load
629 this.checkTodoPanelVisibility();
630
631 // Set up ResizeObserver for chat input to update todo panel height
632 this.setupChatInputObserver();
Sean McCullough86b56862025-04-18 13:04:03 -0700633 }
634
635 // See https://lit.dev/docs/components/lifecycle/
636 disconnectedCallback() {
637 super.disconnectedCallback();
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100638 window.removeEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700639
640 // Remove event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100641 window.removeEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100642 window.removeEventListener("show-commit-diff", this._handleShowCommitDiff);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000643 window.removeEventListener("focus", this._handleWindowFocus);
644 window.removeEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700645 window.removeEventListener(
646 "multiple-choice-selected",
647 this._handleMutlipleChoiceSelected,
648 );
Sean McCullough86b56862025-04-18 13:04:03 -0700649
650 // unregister data manager event listeners
651 this.dataManager.removeEventListener(
652 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700653 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700654 );
655 this.dataManager.removeEventListener(
656 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700657 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700658 );
659
660 // Disconnect mutation observer if it exists
661 if (this.mutationObserver) {
Sean McCullough86b56862025-04-18 13:04:03 -0700662 this.mutationObserver.disconnect();
663 this.mutationObserver = null;
664 }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700665
666 // Disconnect chat input resize observer if it exists
667 if (this.chatInputResizeObserver) {
668 this.chatInputResizeObserver.disconnect();
669 this.chatInputResizeObserver = null;
670 }
Sean McCullough86b56862025-04-18 13:04:03 -0700671 }
672
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700673 updateUrlForViewMode(mode: ViewMode): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700674 // Get the current URL without search parameters
675 const url = new URL(window.location.href);
676
677 // Clear existing parameters
678 url.search = "";
679
680 // Only add view parameter if not in default chat view
681 if (mode !== "chat") {
682 url.searchParams.set("view", mode);
Philip Zeyliger00bcaef2025-05-30 04:21:15 +0000683 const diff2View = this.shadowRoot?.querySelector(
684 "sketch-diff2-view",
685 ) as SketchDiff2View;
Sean McCullough86b56862025-04-18 13:04:03 -0700686
Philip Zeyliger00bcaef2025-05-30 04:21:15 +0000687 // If in diff2 view and there's a commit hash, include that too
688 if (mode === "diff2" && diff2View?.commit) {
689 url.searchParams.set("commit", diff2View.commit);
Sean McCullough86b56862025-04-18 13:04:03 -0700690 }
691 }
692
693 // Update the browser history without reloading the page
694 window.history.pushState({ mode }, "", url.toString());
695 }
696
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100697 private _handlePopState(event: PopStateEvent) {
Sean McCullough86b56862025-04-18 13:04:03 -0700698 if (event.state && event.state.mode) {
699 this.toggleViewMode(event.state.mode, false);
700 } else {
701 this.toggleViewMode("chat", false);
702 }
703 }
704
705 /**
706 * Handle view mode selection event
707 */
708 private _handleViewModeSelect(event: CustomEvent) {
Philip Zeyliger00bcaef2025-05-30 04:21:15 +0000709 const mode = event.detail.mode as "chat" | "diff2" | "terminal";
Sean McCullough86b56862025-04-18 13:04:03 -0700710 this.toggleViewMode(mode, true);
711 }
712
713 /**
714 * Handle show commit diff event
715 */
716 private _handleShowCommitDiff(event: CustomEvent) {
717 const { commitHash } = event.detail;
718 if (commitHash) {
719 this.showCommitDiff(commitHash);
720 }
721 }
722
Sean McCullough485afc62025-04-28 14:28:39 -0700723 private _handleMultipleChoice(event: CustomEvent) {
724 window.console.log("_handleMultipleChoice", event);
725 this._sendChat;
726 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700727
728 private _handleDiffComment(event: CustomEvent) {
729 // Empty stub required by the event binding in the template
730 // Actual handling occurs at global level in sketch-chat-input component
731 }
Sean McCullough86b56862025-04-18 13:04:03 -0700732 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700733 * Listen for commit diff event
734 * @param commitHash The commit hash to show diff for
735 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100736 private showCommitDiff(commitHash: string): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700737 // Store the commit hash
738 this.currentCommitHash = commitHash;
739
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700740 this.toggleViewMode("diff2", true);
Sean McCullough86b56862025-04-18 13:04:03 -0700741
Sean McCullough86b56862025-04-18 13:04:03 -0700742 this.updateComplete.then(() => {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700743 const diff2View = this.shadowRoot?.querySelector("sketch-diff2-view");
744 if (diff2View) {
745 (diff2View as SketchDiff2View).refreshDiffView();
Sean McCullough86b56862025-04-18 13:04:03 -0700746 }
747 });
748 }
749
750 /**
Philip Zeyliger00bcaef2025-05-30 04:21:15 +0000751 * Toggle between different view modes: chat, diff2, terminal
Sean McCullough86b56862025-04-18 13:04:03 -0700752 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100753 private toggleViewMode(mode: ViewMode, updateHistory: boolean): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700754 // Don't do anything if the mode is already active
755 if (this.viewMode === mode) return;
756
757 // Update the view mode
758 this.viewMode = mode;
759
760 if (updateHistory) {
761 // Update URL with the current view mode
762 this.updateUrlForViewMode(mode);
763 }
764
765 // Wait for DOM update to complete
766 this.updateComplete.then(() => {
767 // Update active view
Pokey Rule46fff972025-04-25 14:57:44 +0100768 const viewContainerInner = this.shadowRoot?.querySelector(
769 "#view-container-inner",
770 );
Sean McCullough86b56862025-04-18 13:04:03 -0700771 const chatView = this.shadowRoot?.querySelector(".chat-view");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700772 const diff2View = this.shadowRoot?.querySelector(".diff2-view");
Sean McCullough86b56862025-04-18 13:04:03 -0700773 const terminalView = this.shadowRoot?.querySelector(".terminal-view");
774
775 // Remove active class from all views
776 chatView?.classList.remove("view-active");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700777 diff2View?.classList.remove("view-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700778 terminalView?.classList.remove("view-active");
779
Philip Zeyliger00bcaef2025-05-30 04:21:15 +0000780 // Add/remove diff2-active class on view container
781 if (mode === "diff2") {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700782 viewContainerInner?.classList.add("diff2-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700783 } else {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700784 viewContainerInner?.classList.remove("diff2-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700785 }
786
787 // Add active class to the selected view
788 switch (mode) {
789 case "chat":
790 chatView?.classList.add("view-active");
791 break;
Autoformatter8c463622025-05-16 21:54:17 +0000792
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700793 case "diff2":
794 diff2View?.classList.add("view-active");
795 // Refresh git/recentlog when Monaco diff view is opened
796 // This ensures branch information is always up-to-date, as branches can change frequently
Autoformatter8c463622025-05-16 21:54:17 +0000797 const diff2ViewComp =
798 this.shadowRoot?.querySelector("sketch-diff2-view");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700799 if (diff2ViewComp) {
800 (diff2ViewComp as SketchDiff2View).refreshDiffView();
801 }
802 break;
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000803
Sean McCullough86b56862025-04-18 13:04:03 -0700804 case "terminal":
805 terminalView?.classList.add("view-active");
806 break;
807 }
808
809 // Update view mode buttons
810 const viewModeSelect = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700811 "sketch-view-mode-select",
Sean McCullough86b56862025-04-18 13:04:03 -0700812 );
813 if (viewModeSelect) {
814 const event = new CustomEvent("update-active-mode", {
815 detail: { mode },
816 bubbles: true,
817 composed: true,
818 });
819 viewModeSelect.dispatchEvent(event);
820 }
Sean McCullough86b56862025-04-18 13:04:03 -0700821 });
822 }
823
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000824 /**
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700825 * Updates the document title based on current slug and connection status
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000826 */
827 private updateDocumentTitle(): void {
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700828 let docTitle = `sk: ${this.slug || "untitled"}`;
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000829
830 // Add red circle emoji if disconnected
831 if (this.connectionStatus === "disconnected") {
832 docTitle += " 🔴";
833 }
834
835 document.title = docTitle;
836 }
837
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000838 // Check and request notification permission if needed
839 private async checkNotificationPermission(): Promise<boolean> {
840 // Check if the Notification API is supported
841 if (!("Notification" in window)) {
842 console.log("This browser does not support notifications");
843 return false;
844 }
845
846 // Check if permission is already granted
847 if (Notification.permission === "granted") {
848 return true;
849 }
850
851 // If permission is not denied, request it
852 if (Notification.permission !== "denied") {
853 const permission = await Notification.requestPermission();
854 return permission === "granted";
855 }
856
857 return false;
858 }
859
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000860 // Handle notifications toggle click
861 private _handleNotificationsToggle(): void {
862 this.notificationsEnabled = !this.notificationsEnabled;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000863
864 // If enabling notifications, check permissions
865 if (this.notificationsEnabled) {
866 this.checkNotificationPermission();
867 }
868
869 // Save preference to localStorage
870 try {
871 localStorage.setItem(
872 "sketch-notifications-enabled",
873 String(this.notificationsEnabled),
874 );
875 } catch (error) {
876 console.error("Error saving notification preference:", error);
877 }
878 }
879
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000880 // Handle window focus event
881 private _handleWindowFocus(): void {
882 this._windowFocused = true;
883 }
884
885 // Handle window blur event
886 private _handleWindowBlur(): void {
887 this._windowFocused = false;
888 }
889
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000890 // Show notification for message with EndOfTurn=true
891 private async showEndOfTurnNotification(
892 message: AgentMessage,
893 ): Promise<void> {
894 // Don't show notifications if they're disabled
895 if (!this.notificationsEnabled) return;
896
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000897 // Don't show notifications if the window is focused
898 if (this._windowFocused) return;
899
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000900 // Check if we have permission to show notifications
901 const hasPermission = await this.checkNotificationPermission();
902 if (!hasPermission) return;
903
Philip Zeyliger32011332025-04-30 20:59:40 +0000904 // Only show notifications for agent messages with end_of_turn=true and no parent_conversation_id
905 if (
906 message.type !== "agent" ||
907 !message.end_of_turn ||
908 message.parent_conversation_id
909 )
910 return;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000911
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700912 // Create a title that includes the sketch slug
913 const notificationTitle = `Sketch: ${this.slug || "untitled"}`;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000914
915 // Extract the beginning of the message content (first 100 chars)
916 const messagePreview = message.content
917 ? message.content.substring(0, 100) +
918 (message.content.length > 100 ? "..." : "")
919 : "Agent has completed its turn";
920
921 // Create and show the notification
922 try {
923 new Notification(notificationTitle, {
924 body: messagePreview,
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000925 icon: "https://sketch.dev/favicon.ico", // Use sketch.dev favicon for notification
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000926 });
927 } catch (error) {
928 console.error("Error showing notification:", error);
929 }
930 }
931
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700932 // Check if todo panel should be visible based on latest todo content from messages or state
933 private checkTodoPanelVisibility(): void {
934 // Find the latest todo content from messages first
935 let latestTodoContent = "";
936 for (let i = this.messages.length - 1; i >= 0; i--) {
937 const message = this.messages[i];
938 if (message.todo_content !== undefined) {
939 latestTodoContent = message.todo_content || "";
940 break;
941 }
942 }
943
944 // If no todo content found in messages, check the current state
945 if (latestTodoContent === "" && this.containerState?.todo_content) {
946 latestTodoContent = this.containerState.todo_content;
947 }
948
949 // Parse the todo data to check if there are any actual todos
950 let hasTodos = false;
951 if (latestTodoContent.trim()) {
952 try {
953 const todoData = JSON.parse(latestTodoContent);
954 hasTodos = todoData.items && todoData.items.length > 0;
955 } catch (error) {
956 // Invalid JSON, treat as no todos
957 hasTodos = false;
958 }
959 }
960
961 this._todoPanelVisible = hasTodos;
962
963 // Update todo panel content if visible
964 if (hasTodos) {
965 const todoPanel = this.shadowRoot?.querySelector(
966 "sketch-todo-panel",
967 ) as any;
968 if (todoPanel && todoPanel.updateTodoContent) {
969 todoPanel.updateTodoContent(latestTodoContent);
970 }
971 }
972 }
973
Sean McCullough86b56862025-04-18 13:04:03 -0700974 private handleDataChanged(eventData: {
975 state: State;
Sean McCulloughd9f13372025-04-21 15:08:49 -0700976 newMessages: AgentMessage[];
Sean McCullough86b56862025-04-18 13:04:03 -0700977 }): void {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000978 const { state, newMessages } = eventData;
Sean McCullough86b56862025-04-18 13:04:03 -0700979
980 // Update state if we received it
981 if (state) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000982 // Ensure we're using the latest call status to prevent indicators from being stuck
Autoformatterf830c9d2025-04-30 18:16:01 +0000983 if (
984 state.outstanding_llm_calls === 0 &&
985 state.outstanding_tool_calls.length === 0
986 ) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000987 // Force reset containerState calls when nothing is reported as in progress
988 state.outstanding_llm_calls = 0;
989 state.outstanding_tool_calls = [];
990 }
Autoformatterf830c9d2025-04-30 18:16:01 +0000991
Sean McCullough86b56862025-04-18 13:04:03 -0700992 this.containerState = state;
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700993 this.slug = state.slug || "";
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000994
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700995 // Update document title when sketch slug changes
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000996 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700997 }
998
Sean McCullough86b56862025-04-18 13:04:03 -0700999 // Update messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +01001000 this.messages = aggregateAgentMessages(this.messages, newMessages);
Autoformattercf570962025-04-30 17:27:39 +00001001
Philip Zeyliger47b71c92025-04-30 15:43:39 +00001002 // Process new messages to find commit messages
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001003 // Update last commit info via container status component
1004 if (this.containerStatusElement) {
1005 this.containerStatusElement.updateLastCommitInfo(newMessages);
1006 }
Philip Zeyligerbc6b6292025-04-30 18:00:15 +00001007
1008 // Check for agent messages with end_of_turn=true and show notifications
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001009 if (newMessages && newMessages.length > 0) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +00001010 for (const message of newMessages) {
Philip Zeyliger32011332025-04-30 20:59:40 +00001011 if (
1012 message.type === "agent" &&
1013 message.end_of_turn &&
1014 !message.parent_conversation_id
1015 ) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +00001016 this.showEndOfTurnNotification(message);
1017 break; // Only show one notification per batch of messages
1018 }
1019 }
1020 }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001021
1022 // Check if todo panel should be visible after agent loop iteration
1023 this.checkTodoPanelVisibility();
1024
1025 // Ensure chat input observer is set up when new data comes in
1026 if (!this.chatInputResizeObserver) {
1027 this.setupChatInputObserver();
1028 }
Sean McCullough86b56862025-04-18 13:04:03 -07001029 }
1030
1031 private handleConnectionStatusChanged(
1032 status: ConnectionStatus,
Philip Zeyliger72682df2025-04-23 13:09:46 -07001033 errorMessage?: string,
Sean McCullough86b56862025-04-18 13:04:03 -07001034 ): void {
1035 this.connectionStatus = status;
1036 this.connectionErrorMessage = errorMessage || "";
Philip Zeyliger9b999b02025-04-25 16:31:50 +00001037
1038 // Update document title when connection status changes
1039 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -07001040 }
1041
Sean McCulloughd3906e22025-04-29 17:32:14 +00001042 private async _handleStopClick(): Promise<void> {
1043 try {
1044 const response = await fetch("cancel", {
1045 method: "POST",
1046 headers: {
1047 "Content-Type": "application/json",
1048 },
1049 body: JSON.stringify({ reason: "user requested cancellation" }),
1050 });
1051
1052 if (!response.ok) {
1053 const errorData = await response.text();
1054 throw new Error(
1055 `Failed to stop operation: ${response.status} - ${errorData}`,
1056 );
1057 }
1058
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001059 // Stop request sent
Sean McCulloughd3906e22025-04-29 17:32:14 +00001060 } catch (error) {
1061 console.error("Error stopping operation:", error);
Sean McCulloughd3906e22025-04-29 17:32:14 +00001062 }
1063 }
1064
Pokey Rule397871d2025-05-19 15:02:45 +01001065 private async _handleEndClick(event?: Event): Promise<void> {
1066 if (event) {
1067 event.preventDefault();
1068 event.stopPropagation();
1069 }
Philip Zeyligerb5739402025-06-02 07:04:34 -07001070
Philip Zeyliger16098932025-06-04 11:02:55 -07001071 // Show confirmation dialog
1072 const confirmed = window.confirm(
1073 "Ending the session will shut down the underlying container. Are you sure?",
1074 );
1075 if (!confirmed) return;
Pokey Rule397871d2025-05-19 15:02:45 +01001076
1077 try {
1078 const response = await fetch("end", {
1079 method: "POST",
1080 headers: {
1081 "Content-Type": "application/json",
1082 },
Philip Zeyliger16098932025-06-04 11:02:55 -07001083 body: JSON.stringify({ reason: "user requested end of session" }),
Pokey Rule397871d2025-05-19 15:02:45 +01001084 });
1085
1086 if (!response.ok) {
1087 const errorData = await response.text();
1088 throw new Error(
1089 `Failed to end session: ${response.status} - ${errorData}`,
1090 );
1091 }
1092
1093 // After successful response, redirect to messages view
1094 // Extract the session ID from the URL
1095 const currentUrl = window.location.href;
1096 // The URL pattern should be like https://sketch.dev/s/cs71-8qa6-1124-aw79/
1097 const urlParts = currentUrl.split("/");
1098 let sessionId = "";
1099
1100 // Find the session ID in the URL (should be after /s/)
1101 for (let i = 0; i < urlParts.length; i++) {
1102 if (urlParts[i] === "s" && i + 1 < urlParts.length) {
1103 sessionId = urlParts[i + 1];
1104 break;
1105 }
1106 }
1107
1108 if (sessionId) {
1109 // Create the messages URL
1110 const messagesUrl = `/messages/${sessionId}`;
1111 // Redirect to messages view
1112 window.location.href = messagesUrl;
1113 }
1114
1115 // End request sent - connection will be closed by server
1116 } catch (error) {
1117 console.error("Error ending session:", error);
1118 }
1119 }
1120
Sean McCullough485afc62025-04-28 14:28:39 -07001121 async _handleMutlipleChoiceSelected(e: CustomEvent) {
1122 const chatInput = this.shadowRoot?.querySelector(
1123 "sketch-chat-input",
1124 ) as SketchChatInput;
1125 if (chatInput) {
Josh Bleecher Snyder6cad8612025-05-30 19:25:39 +00001126 if (chatInput.content && chatInput.content.trim() !== "") {
1127 chatInput.content += "\n\n";
1128 }
1129 chatInput.content += e.detail.responseText;
Sean McCullough485afc62025-04-28 14:28:39 -07001130 chatInput.focus();
Josh Bleecher Snyder6cad8612025-05-30 19:25:39 +00001131 // Adjust textarea height to accommodate new content
1132 requestAnimationFrame(() => {
1133 if (chatInput.adjustChatSpacing) {
1134 chatInput.adjustChatSpacing();
1135 }
1136 });
Sean McCullough485afc62025-04-28 14:28:39 -07001137 }
1138 }
1139
Sean McCullough86b56862025-04-18 13:04:03 -07001140 async _sendChat(e: CustomEvent) {
1141 console.log("app shell: _sendChat", e);
Sean McCullough485afc62025-04-28 14:28:39 -07001142 e.preventDefault();
1143 e.stopPropagation();
Sean McCullough86b56862025-04-18 13:04:03 -07001144 const message = e.detail.message?.trim();
1145 if (message == "") {
1146 return;
1147 }
1148 try {
Josh Bleecher Snyder98b64d12025-05-12 19:42:43 +00001149 // Always switch to chat view when sending a message so user can see processing
1150 if (this.viewMode !== "chat") {
1151 this.toggleViewMode("chat", true);
1152 }
Autoformatter5c7f9572025-05-13 01:17:31 +00001153
Sean McCullough86b56862025-04-18 13:04:03 -07001154 // Send the message to the server
1155 const response = await fetch("chat", {
1156 method: "POST",
1157 headers: {
1158 "Content-Type": "application/json",
1159 },
1160 body: JSON.stringify({ message }),
1161 });
1162
1163 if (!response.ok) {
1164 const errorData = await response.text();
1165 throw new Error(`Server error: ${response.status} - ${errorData}`);
1166 }
Sean McCullough86b56862025-04-18 13:04:03 -07001167 } catch (error) {
1168 console.error("Error sending chat message:", error);
1169 const statusText = document.getElementById("statusText");
1170 if (statusText) {
1171 statusText.textContent = "Error sending message";
1172 }
1173 }
1174 }
1175
Pokey Rule4097e532025-04-24 18:55:28 +01001176 private scrollContainerRef = createRef<HTMLElement>();
1177
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001178 /**
1179 * Set up ResizeObserver to monitor chat input height changes
1180 */
1181 private setupChatInputObserver(): void {
1182 // Wait for DOM to be ready
1183 this.updateComplete.then(() => {
1184 const chatInputElement = this.shadowRoot?.querySelector("#chat-input");
1185 if (chatInputElement && !this.chatInputResizeObserver) {
1186 this.chatInputResizeObserver = new ResizeObserver((entries) => {
1187 for (const entry of entries) {
1188 this.updateTodoPanelHeight(entry.contentRect.height);
1189 }
1190 });
1191
1192 this.chatInputResizeObserver.observe(chatInputElement);
1193
1194 // Initial height calculation
1195 const rect = chatInputElement.getBoundingClientRect();
1196 this.updateTodoPanelHeight(rect.height);
1197 }
1198 });
1199 }
1200
1201 /**
1202 * Update the CSS custom property that controls todo panel bottom position
1203 */
1204 private updateTodoPanelHeight(chatInputHeight: number): void {
1205 // Add some padding (20px) between todo panel and chat input
1206 const bottomOffset = chatInputHeight;
1207
1208 // Update the CSS custom property on the host element
1209 this.style.setProperty("--chat-input-height", `${bottomOffset}px`);
1210 }
1211
Sean McCullough86b56862025-04-18 13:04:03 -07001212 render() {
1213 return html`
Pokey Rule4097e532025-04-24 18:55:28 +01001214 <div id="top-banner">
Sean McCullough86b56862025-04-18 13:04:03 -07001215 <div class="title-container">
Philip Zeyliger0113be52025-06-07 23:53:41 +00001216 <h1 class="banner-title">
1217 ${this.containerState?.skaband_addr
1218 ? html`<a
1219 href="${this.containerState.skaband_addr}"
1220 target="_blank"
1221 rel="noopener noreferrer"
1222 >
1223 <img
1224 src="${this.containerState.skaband_addr}/sketch.dev.png"
1225 alt="sketch"
1226 />
1227 sketch
1228 </a>`
1229 : html`sketch`}
1230 </h1>
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -07001231 <h2 class="slug-title">${this.slug}</h2>
Sean McCullough86b56862025-04-18 13:04:03 -07001232 </div>
1233
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001234 <!-- Container status info moved above tabs -->
Sean McCullough86b56862025-04-18 13:04:03 -07001235 <sketch-container-status
1236 .state=${this.containerState}
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001237 id="container-status"
Sean McCullough86b56862025-04-18 13:04:03 -07001238 ></sketch-container-status>
Autoformattercf570962025-04-30 17:27:39 +00001239
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001240 <!-- Last Commit section moved to sketch-container-status -->
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001241
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001242 <!-- Views section with tabs -->
Philip Zeyliger64f60462025-06-16 13:57:10 -07001243 <sketch-view-mode-select
1244 .diffLinesAdded=${this.containerState?.diff_lines_added || 0}
1245 .diffLinesRemoved=${this.containerState?.diff_lines_removed || 0}
1246 ></sketch-view-mode-select>
Sean McCullough86b56862025-04-18 13:04:03 -07001247
1248 <div class="refresh-control">
Sean McCulloughd3906e22025-04-29 17:32:14 +00001249 <button
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001250 id="stopButton"
1251 class="stop-button"
1252 ?disabled=${(this.containerState?.outstanding_llm_calls || 0) ===
1253 0 &&
1254 (this.containerState?.outstanding_tool_calls || []).length === 0}
1255 >
1256 <svg
1257 class="button-icon"
1258 xmlns="http://www.w3.org/2000/svg"
1259 viewBox="0 0 24 24"
1260 fill="none"
1261 stroke="currentColor"
1262 stroke-width="2"
1263 stroke-linecap="round"
1264 stroke-linejoin="round"
1265 >
1266 <rect x="6" y="6" width="12" height="12" />
1267 </svg>
1268 <span class="button-text">Stop</span>
Sean McCullough86b56862025-04-18 13:04:03 -07001269 </button>
Pokey Rule397871d2025-05-19 15:02:45 +01001270 <button
1271 id="endButton"
1272 class="end-button"
1273 @click=${this._handleEndClick}
1274 >
1275 <svg
1276 class="button-icon"
1277 xmlns="http://www.w3.org/2000/svg"
1278 viewBox="0 0 24 24"
1279 fill="none"
1280 stroke="currentColor"
1281 stroke-width="2"
1282 stroke-linecap="round"
1283 stroke-linejoin="round"
1284 >
1285 <path d="M18 6L6 18" />
1286 <path d="M6 6l12 12" />
1287 </svg>
1288 <span class="button-text">End</span>
1289 </button>
Sean McCullough86b56862025-04-18 13:04:03 -07001290
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +00001291 <div
1292 class="notifications-toggle"
1293 @click=${this._handleNotificationsToggle}
1294 title="${this.notificationsEnabled
1295 ? "Disable"
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001296 : "Enable"} notifications when the agent completes its turn"
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +00001297 >
1298 <div
1299 class="bell-icon ${!this.notificationsEnabled
1300 ? "bell-disabled"
1301 : ""}"
1302 >
1303 <!-- Bell SVG icon -->
1304 <svg
1305 xmlns="http://www.w3.org/2000/svg"
1306 width="16"
1307 height="16"
1308 fill="currentColor"
1309 viewBox="0 0 16 16"
1310 >
1311 <path
1312 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"
1313 />
1314 </svg>
1315 </div>
Philip Zeyligerbc6b6292025-04-30 18:00:15 +00001316 </div>
1317
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001318 <sketch-call-status
Sean McCulloughd9d45812025-04-30 16:53:41 -07001319 .agentState=${this.containerState?.agent_state}
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001320 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
1321 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
Philip Zeyliger72318392025-05-14 02:56:07 +00001322 .isIdle=${this.messages.length > 0
1323 ? this.messages[this.messages.length - 1]?.end_of_turn &&
1324 !this.messages[this.messages.length - 1]?.parent_conversation_id
1325 : true}
Philip Zeyliger5e357022025-05-16 04:50:34 +00001326 .isDisconnected=${this.connectionStatus === "disconnected"}
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001327 ></sketch-call-status>
Philip Zeyliger25f6ff12025-05-02 04:24:10 +00001328
1329 <sketch-network-status
1330 connection=${this.connectionStatus}
1331 error=${this.connectionErrorMessage}
1332 ></sketch-network-status>
Sean McCullough86b56862025-04-18 13:04:03 -07001333 </div>
1334 </div>
1335
Pokey Rule4097e532025-04-24 18:55:28 +01001336 <div id="view-container" ${ref(this.scrollContainerRef)}>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001337 <div
1338 id="view-container-inner"
1339 class="${this._todoPanelVisible && this.viewMode === "chat"
1340 ? "with-todo-panel"
1341 : ""}"
1342 >
Pokey Rule4097e532025-04-24 18:55:28 +01001343 <div
1344 class="chat-view ${this.viewMode === "chat" ? "view-active" : ""}"
1345 >
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001346 <div
1347 class="chat-timeline-container ${this._todoPanelVisible &&
1348 this.viewMode === "chat"
1349 ? "with-todo-panel"
1350 : ""}"
1351 >
1352 <sketch-timeline
1353 .messages=${this.messages}
1354 .scrollContainer=${this.scrollContainerRef}
1355 .agentState=${this.containerState?.agent_state}
1356 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
1357 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001358 .firstMessageIndex=${this.containerState?.first_message_index ||
1359 0}
philip.zeyliger6d3de482025-06-10 19:38:14 -07001360 .state=${this.containerState}
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001361 ></sketch-timeline>
1362 </div>
1363 </div>
1364
1365 <!-- Todo panel positioned outside the main flow - only visible in chat view -->
1366 <div
1367 class="todo-panel-container ${this._todoPanelVisible &&
1368 this.viewMode === "chat"
1369 ? "visible"
1370 : ""}"
1371 >
1372 <sketch-todo-panel
1373 .visible=${this._todoPanelVisible && this.viewMode === "chat"}
1374 ></sketch-todo-panel>
Pokey Rule4097e532025-04-24 18:55:28 +01001375 </div>
1376 <div
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001377 class="diff2-view ${this.viewMode === "diff2" ? "view-active" : ""}"
1378 >
1379 <sketch-diff2-view
1380 .commit=${this.currentCommitHash}
1381 .gitService=${new DefaultGitDataService()}
1382 @diff-comment="${this._handleDiffComment}"
1383 ></sketch-diff2-view>
1384 </div>
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +00001385
Pokey Rule4097e532025-04-24 18:55:28 +01001386 <div
1387 class="terminal-view ${this.viewMode === "terminal"
1388 ? "view-active"
1389 : ""}"
1390 >
1391 <sketch-terminal></sketch-terminal>
1392 </div>
Sean McCullough86b56862025-04-18 13:04:03 -07001393 </div>
1394 </div>
1395
Pokey Rule4097e532025-04-24 18:55:28 +01001396 <div id="chat-input">
1397 <sketch-chat-input @send-chat="${this._sendChat}"></sketch-chat-input>
1398 </div>
Sean McCullough86b56862025-04-18 13:04:03 -07001399 `;
1400 }
1401
1402 /**
Sean McCullough86b56862025-04-18 13:04:03 -07001403 * Lifecycle callback when component is first connected to DOM
1404 */
1405 firstUpdated(): void {
1406 if (this.viewMode !== "chat") {
1407 return;
1408 }
1409
1410 // Initial scroll to bottom when component is first rendered
1411 setTimeout(
1412 () => this.scrollTo({ top: this.scrollHeight, behavior: "smooth" }),
Philip Zeyliger72682df2025-04-23 13:09:46 -07001413 50,
Sean McCullough86b56862025-04-18 13:04:03 -07001414 );
1415
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001416 // Setup stop button
1417 const stopButton = this.renderRoot?.querySelector(
1418 "#stopButton",
1419 ) as HTMLButtonElement;
1420 stopButton?.addEventListener("click", async () => {
1421 try {
Sean McCullough495cb962025-05-01 16:25:53 -07001422 const response = await fetch("cancel", {
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001423 method: "POST",
1424 headers: {
1425 "Content-Type": "application/json",
1426 },
1427 body: JSON.stringify({ reason: "User clicked stop button" }),
1428 });
1429 if (!response.ok) {
1430 console.error("Failed to cancel:", await response.text());
1431 }
1432 } catch (error) {
1433 console.error("Error cancelling operation:", error);
1434 }
1435 });
1436
Pokey Rule397871d2025-05-19 15:02:45 +01001437 // Setup end button
1438 const endButton = this.renderRoot?.querySelector(
1439 "#endButton",
1440 ) as HTMLButtonElement;
1441 // We're already using the @click binding in the HTML, so manual event listener not needed here
1442
Philip Zeyliger47b71c92025-04-30 15:43:39 +00001443 // Process any existing messages to find commit information
1444 if (this.messages && this.messages.length > 0) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001445 // Update last commit info via container status component
1446 if (this.containerStatusElement) {
1447 this.containerStatusElement.updateLastCommitInfo(this.messages);
1448 }
Philip Zeyliger47b71c92025-04-30 15:43:39 +00001449 }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001450
1451 // Set up chat input height observer for todo panel
1452 this.setupChatInputObserver();
Sean McCullough86b56862025-04-18 13:04:03 -07001453 }
1454}
1455
1456declare global {
1457 interface HTMLElementTagNameMap {
1458 "sketch-app-shell": SketchAppShell;
1459 }
1460}