blob: 81d2fadbd077a9a1a8d445f5e31c9f6185695fd1 [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";
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070020import "./sketch-todo-panel";
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
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700143 /* Adjust view container when todo panel is visible in chat mode */
144 #view-container-inner.with-todo-panel {
145 max-width: none;
146 width: 100%;
147 margin: 0;
148 padding-left: 20px;
149 padding-right: 20px;
150 }
151
Pokey Rule4097e532025-04-24 18:55:28 +0100152 #chat-input {
153 align-self: flex-end;
154 width: 100%;
155 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
156 }
157
Sean McCullough86b56862025-04-18 13:04:03 -0700158 .banner-title {
159 font-size: 18px;
160 font-weight: 600;
161 margin: 0;
162 min-width: 6em;
163 white-space: nowrap;
164 overflow: hidden;
165 text-overflow: ellipsis;
166 }
167
168 .chat-title {
169 margin: 0;
170 padding: 0;
171 color: rgba(82, 82, 82, 0.85);
Josh Bleecher Snydereb5166a2025-04-30 17:04:20 +0000172 font-size: 14px;
Sean McCullough86b56862025-04-18 13:04:03 -0700173 font-weight: normal;
174 font-style: italic;
175 white-space: nowrap;
176 overflow: hidden;
177 text-overflow: ellipsis;
178 }
179
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700180 /* Allow the container to expand to full width and height in diff mode */
181 #view-container-inner.diff-active,
182 #view-container-inner.diff2-active {
Sean McCullough86b56862025-04-18 13:04:03 -0700183 max-width: 100%;
Pokey Rule46fff972025-04-25 14:57:44 +0100184 width: 100%;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700185 height: 100%;
186 padding: 0; /* Remove padding for more space */
187 display: flex;
188 flex-direction: column;
189 flex: 1;
190 min-height: 0; /* Critical for flex behavior */
Sean McCullough86b56862025-04-18 13:04:03 -0700191 }
192
193 /* Individual view styles */
194 .chat-view,
195 .diff-view,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700196 .diff2-view,
Sean McCullough86b56862025-04-18 13:04:03 -0700197 .terminal-view {
198 display: none; /* Hidden by default */
199 width: 100%;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700200 height: 100%;
201 }
Autoformatter8c463622025-05-16 21:54:17 +0000202
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700203 /* Make chat view take full width available */
204 .chat-view.view-active {
205 display: flex;
206 flex-direction: column;
207 width: 100%;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700208 height: 100%;
209 }
210
211 /* Chat timeline container - takes full width, memory panel will be positioned separately */
212 .chat-timeline-container {
213 flex: 1;
214 display: flex;
215 flex-direction: column;
216 width: 100%;
217 height: 100%;
218 margin-right: 0; /* Default - no memory panel */
219 transition: margin-right 0.2s ease; /* Smooth transition */
220 }
221
222 /* Adjust chat timeline container when todo panel is visible */
223 .chat-timeline-container.with-todo-panel {
224 margin-right: 400px; /* Make space for fixed todo panel */
225 width: calc(100% - 400px); /* Explicitly set width to prevent overlap */
226 }
227
228 /* Todo panel container - fixed to right side */
229 .todo-panel-container {
230 position: fixed;
231 top: 48px; /* Below top banner */
232 right: 15px; /* Leave space for scroll bar */
233 width: 400px;
234 bottom: var(
235 --chat-input-height,
236 90px
237 ); /* Dynamic height based on chat input size */
238 background-color: #fafafa;
239 border-left: 1px solid #e0e0e0;
240 z-index: 100;
241 display: none; /* Hidden by default */
242 transition: bottom 0.2s ease; /* Smooth transition when height changes */
243 /* Add fuzzy gradient at bottom to blend with text entry */
244 background: linear-gradient(
245 to bottom,
246 #fafafa 0%,
247 #fafafa 90%,
248 rgba(250, 250, 250, 0.5) 95%,
249 rgba(250, 250, 250, 0.2) 100%
250 );
251 }
252
253 .todo-panel-container.visible {
254 display: block;
255 }
256
257 /* Responsive adjustments for todo panel */
258 @media (max-width: 1200px) {
259 .todo-panel-container {
260 width: 350px;
261 /* bottom is still controlled by --chat-input-height CSS variable */
262 }
263 .chat-timeline-container.with-todo-panel {
264 margin-right: 350px;
265 width: calc(100% - 350px);
266 }
267 }
268
269 @media (max-width: 900px) {
270 .todo-panel-container {
271 width: 300px;
272 /* bottom is still controlled by --chat-input-height CSS variable */
273 }
274 .chat-timeline-container.with-todo-panel {
275 margin-right: 300px;
276 width: calc(100% - 300px);
277 }
278 }
279
280 /* On very small screens, hide todo panel or make it overlay */
281 @media (max-width: 768px) {
282 .todo-panel-container.visible {
283 display: none; /* Hide on mobile */
284 }
285 .chat-timeline-container.with-todo-panel {
286 margin-right: 0;
287 width: 100%;
288 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700289 }
Autoformatter8c463622025-05-16 21:54:17 +0000290
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700291 /* Monaco diff2 view needs to take all available space */
292 .diff2-view.view-active {
293 flex: 1;
294 overflow: hidden;
295 min-height: 0; /* Required for proper flex child behavior */
296 display: flex;
297 flex-direction: column;
298 height: 100%;
Sean McCullough86b56862025-04-18 13:04:03 -0700299 }
300
301 /* Active view styles - these will be applied via JavaScript */
302 .view-active {
303 display: flex;
304 flex-direction: column;
305 }
306
307 .title-container {
308 display: flex;
309 flex-direction: column;
310 white-space: nowrap;
311 overflow: hidden;
312 text-overflow: ellipsis;
Josh Bleecher Snydereb5166a2025-04-30 17:04:20 +0000313 max-width: 30%;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000314 padding: 6px 0;
Sean McCullough86b56862025-04-18 13:04:03 -0700315 }
316
317 .refresh-control {
318 display: flex;
319 align-items: center;
320 margin-bottom: 0;
321 flex-wrap: nowrap;
322 white-space: nowrap;
323 flex-shrink: 0;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000324 gap: 15px;
325 padding-left: 15px;
326 margin-right: 50px;
Sean McCullough86b56862025-04-18 13:04:03 -0700327 }
328
Pokey Rule397871d2025-05-19 15:02:45 +0100329 .stop-button,
330 .end-button {
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700331 background: #2196f3;
332 color: white;
333 border: none;
334 padding: 4px 10px;
335 border-radius: 4px;
336 cursor: pointer;
337 font-size: 12px;
338 margin-right: 5px;
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000339 display: flex;
340 align-items: center;
341 gap: 6px;
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700342 }
343
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000344 .stop-button {
345 background: #dc3545;
Sean McCullough86b56862025-04-18 13:04:03 -0700346 color: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700347 }
348
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000349 .stop-button:hover:not(:disabled) {
350 background-color: #c82333;
Sean McCullough86b56862025-04-18 13:04:03 -0700351 }
352
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000353 .stop-button:disabled {
354 background-color: #e9a8ad;
355 cursor: not-allowed;
356 opacity: 0.7;
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000357 }
358
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000359 .stop-button:disabled:hover {
360 background-color: #e9a8ad;
361 }
362
Pokey Rule397871d2025-05-19 15:02:45 +0100363 .end-button {
364 background: #6c757d;
365 color: white;
366 }
367
368 .end-button:hover:not(:disabled) {
369 background-color: #5a6268;
370 }
371
372 .end-button:disabled {
373 background-color: #a9acaf;
374 cursor: not-allowed;
375 opacity: 0.7;
376 }
377
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000378 .button-icon {
379 width: 16px;
380 height: 16px;
381 }
382
383 @media (max-width: 1400px) {
384 .button-text {
385 display: none;
386 }
387
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000388 .stop-button {
389 padding: 6px;
390 }
391 }
392
393 /* Removed poll-updates class */
394
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000395 .notifications-toggle {
Sean McCullough86b56862025-04-18 13:04:03 -0700396 display: flex;
397 align-items: center;
Sean McCullough86b56862025-04-18 13:04:03 -0700398 font-size: 12px;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000399 margin-right: 10px;
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000400 cursor: pointer;
401 }
402
403 .bell-icon {
404 width: 20px;
405 height: 20px;
406 position: relative;
407 display: inline-flex;
408 align-items: center;
409 justify-content: center;
410 }
411
412 .bell-disabled::before {
413 content: "";
414 position: absolute;
415 width: 2px;
416 height: 24px;
417 background-color: #dc3545;
418 transform: rotate(45deg);
419 transform-origin: center center;
Sean McCullough86b56862025-04-18 13:04:03 -0700420 }
421 `;
422
423 // Header bar: Network connection status details
424 @property()
425 connectionStatus: ConnectionStatus = "disconnected";
Autoformattercf570962025-04-30 17:27:39 +0000426
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000427 // Track if the last commit info has been copied
428 @state()
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000429 // lastCommitCopied moved to sketch-container-status
Sean McCullough86b56862025-04-18 13:04:03 -0700430
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000431 // Track notification preferences
432 @state()
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000433 notificationsEnabled: boolean = false;
434
435 // Track if the window is focused to control notifications
436 @state()
437 private _windowFocused: boolean = document.hasFocus();
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000438
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700439 // Track if the todo panel should be visible
440 @state()
441 private _todoPanelVisible: boolean = false;
442
443 // ResizeObserver for tracking chat input height changes
444 private chatInputResizeObserver: ResizeObserver | null = null;
445
Sean McCullough86b56862025-04-18 13:04:03 -0700446 @property()
447 connectionErrorMessage: string = "";
448
Sean McCullough86b56862025-04-18 13:04:03 -0700449 // Chat messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100450 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700451 messages: AgentMessage[] = [];
Sean McCullough86b56862025-04-18 13:04:03 -0700452
453 @property()
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000454 set title(value: string) {
455 const oldValue = this._title;
456 this._title = value;
457 this.requestUpdate("title", oldValue);
458 // Update document title when title property changes
459 this.updateDocumentTitle();
460 }
461
462 get title(): string {
463 return this._title;
464 }
465
466 private _title: string = "";
Sean McCullough86b56862025-04-18 13:04:03 -0700467
468 private dataManager = new DataManager();
469
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100470 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700471 containerState: State = {
Philip Zeyligerd03318d2025-05-08 13:09:12 -0700472 state_version: 2,
Sean McCulloughd9f13372025-04-21 15:08:49 -0700473 title: "",
474 os: "",
475 message_count: 0,
476 hostname: "",
477 working_dir: "",
478 initial_commit: "",
Philip Zeyliger99a9a022025-04-27 15:15:25 +0000479 outstanding_llm_calls: 0,
480 outstanding_tool_calls: [],
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000481 session_id: "",
482 ssh_available: false,
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700483 ssh_error: "",
484 in_container: false,
485 first_message_index: 0,
Sean McCulloughd9f13372025-04-21 15:08:49 -0700486 };
Sean McCullough86b56862025-04-18 13:04:03 -0700487
Sean McCullough86b56862025-04-18 13:04:03 -0700488 // Mutation observer to detect when new messages are added
489 private mutationObserver: MutationObserver | null = null;
490
491 constructor() {
492 super();
493
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000494 // Reference to the container status element
495 this.containerStatusElement = null;
496
Sean McCullough86b56862025-04-18 13:04:03 -0700497 // Binding methods to this
498 this._handleViewModeSelect = this._handleViewModeSelect.bind(this);
Sean McCullough34bb09a2025-05-13 15:39:54 -0700499 this._handlePopState = this._handlePopState.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -0700500 this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
Sean McCullough485afc62025-04-28 14:28:39 -0700501 this._handleMutlipleChoiceSelected =
502 this._handleMutlipleChoiceSelected.bind(this);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000503 this._handleStopClick = this._handleStopClick.bind(this);
Pokey Rule397871d2025-05-19 15:02:45 +0100504 this._handleEndClick = this._handleEndClick.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000505 this._handleNotificationsToggle =
506 this._handleNotificationsToggle.bind(this);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000507 this._handleWindowFocus = this._handleWindowFocus.bind(this);
508 this._handleWindowBlur = this._handleWindowBlur.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000509
510 // Load notification preference from localStorage
511 try {
512 const savedPref = localStorage.getItem("sketch-notifications-enabled");
513 if (savedPref !== null) {
514 this.notificationsEnabled = savedPref === "true";
515 }
516 } catch (error) {
517 console.error("Error loading notification preference:", error);
518 }
Sean McCullough86b56862025-04-18 13:04:03 -0700519 }
520
521 // See https://lit.dev/docs/components/lifecycle/
522 connectedCallback() {
523 super.connectedCallback();
524
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000525 // Get reference to the container status element
526 setTimeout(() => {
527 this.containerStatusElement =
528 this.shadowRoot?.getElementById("container-status");
529 }, 0);
530
Sean McCullough86b56862025-04-18 13:04:03 -0700531 // Initialize client-side nav history.
532 const url = new URL(window.location.href);
533 const mode = url.searchParams.get("view") || "chat";
534 window.history.replaceState({ mode }, "", url.toString());
535
536 this.toggleViewMode(mode as ViewMode, false);
537 // Add popstate event listener to handle browser back/forward navigation
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100538 window.addEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700539
540 // Add event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100541 window.addEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100542 window.addEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700543
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000544 // Add window focus/blur listeners for controlling notifications
545 window.addEventListener("focus", this._handleWindowFocus);
546 window.addEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700547 window.addEventListener(
548 "multiple-choice-selected",
549 this._handleMutlipleChoiceSelected,
550 );
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000551
Sean McCullough86b56862025-04-18 13:04:03 -0700552 // register event listeners
553 this.dataManager.addEventListener(
554 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700555 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700556 );
557 this.dataManager.addEventListener(
558 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700559 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700560 );
561
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000562 // Set initial document title
563 this.updateDocumentTitle();
564
Sean McCullough86b56862025-04-18 13:04:03 -0700565 // Initialize the data manager
566 this.dataManager.initialize();
Autoformattercf570962025-04-30 17:27:39 +0000567
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000568 // Process existing messages for commit info
569 if (this.messages && this.messages.length > 0) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000570 // Update last commit info via container status component
571 setTimeout(() => {
572 if (this.containerStatusElement) {
573 this.containerStatusElement.updateLastCommitInfo(this.messages);
574 }
575 }, 100);
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000576 }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700577
578 // Check if todo panel should be visible on initial load
579 this.checkTodoPanelVisibility();
580
581 // Set up ResizeObserver for chat input to update todo panel height
582 this.setupChatInputObserver();
Sean McCullough86b56862025-04-18 13:04:03 -0700583 }
584
585 // See https://lit.dev/docs/components/lifecycle/
586 disconnectedCallback() {
587 super.disconnectedCallback();
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100588 window.removeEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700589
590 // Remove event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100591 window.removeEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100592 window.removeEventListener("show-commit-diff", this._handleShowCommitDiff);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000593 window.removeEventListener("focus", this._handleWindowFocus);
594 window.removeEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700595 window.removeEventListener(
596 "multiple-choice-selected",
597 this._handleMutlipleChoiceSelected,
598 );
Sean McCullough86b56862025-04-18 13:04:03 -0700599
600 // unregister data manager event listeners
601 this.dataManager.removeEventListener(
602 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700603 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700604 );
605 this.dataManager.removeEventListener(
606 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700607 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700608 );
609
610 // Disconnect mutation observer if it exists
611 if (this.mutationObserver) {
Sean McCullough86b56862025-04-18 13:04:03 -0700612 this.mutationObserver.disconnect();
613 this.mutationObserver = null;
614 }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700615
616 // Disconnect chat input resize observer if it exists
617 if (this.chatInputResizeObserver) {
618 this.chatInputResizeObserver.disconnect();
619 this.chatInputResizeObserver = null;
620 }
Sean McCullough86b56862025-04-18 13:04:03 -0700621 }
622
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700623 updateUrlForViewMode(mode: ViewMode): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700624 // Get the current URL without search parameters
625 const url = new URL(window.location.href);
626
627 // Clear existing parameters
628 url.search = "";
629
630 // Only add view parameter if not in default chat view
631 if (mode !== "chat") {
632 url.searchParams.set("view", mode);
Sean McCullough71941bd2025-04-18 13:31:48 -0700633 const diffView = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700634 ".diff-view",
Sean McCullough71941bd2025-04-18 13:31:48 -0700635 ) as SketchDiffView;
Sean McCullough86b56862025-04-18 13:04:03 -0700636
637 // If in diff view and there's a commit hash, include that too
638 if (mode === "diff" && diffView.commitHash) {
639 url.searchParams.set("commit", diffView.commitHash);
640 }
641 }
642
643 // Update the browser history without reloading the page
644 window.history.pushState({ mode }, "", url.toString());
645 }
646
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100647 private _handlePopState(event: PopStateEvent) {
Sean McCullough86b56862025-04-18 13:04:03 -0700648 if (event.state && event.state.mode) {
649 this.toggleViewMode(event.state.mode, false);
650 } else {
651 this.toggleViewMode("chat", false);
652 }
653 }
654
655 /**
656 * Handle view mode selection event
657 */
658 private _handleViewModeSelect(event: CustomEvent) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700659 const mode = event.detail.mode as "chat" | "diff" | "diff2" | "terminal";
Sean McCullough86b56862025-04-18 13:04:03 -0700660 this.toggleViewMode(mode, true);
661 }
662
663 /**
664 * Handle show commit diff event
665 */
666 private _handleShowCommitDiff(event: CustomEvent) {
667 const { commitHash } = event.detail;
668 if (commitHash) {
669 this.showCommitDiff(commitHash);
670 }
671 }
672
Sean McCullough485afc62025-04-28 14:28:39 -0700673 private _handleMultipleChoice(event: CustomEvent) {
674 window.console.log("_handleMultipleChoice", event);
675 this._sendChat;
676 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700677
678 private _handleDiffComment(event: CustomEvent) {
679 // Empty stub required by the event binding in the template
680 // Actual handling occurs at global level in sketch-chat-input component
681 }
Sean McCullough86b56862025-04-18 13:04:03 -0700682 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700683 * Listen for commit diff event
684 * @param commitHash The commit hash to show diff for
685 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100686 private showCommitDiff(commitHash: string): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700687 // Store the commit hash
688 this.currentCommitHash = commitHash;
689
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700690 this.toggleViewMode("diff2", true);
Sean McCullough86b56862025-04-18 13:04:03 -0700691
Sean McCullough86b56862025-04-18 13:04:03 -0700692 this.updateComplete.then(() => {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700693 const diff2View = this.shadowRoot?.querySelector("sketch-diff2-view");
694 if (diff2View) {
695 (diff2View as SketchDiff2View).refreshDiffView();
Sean McCullough86b56862025-04-18 13:04:03 -0700696 }
697 });
698 }
699
700 /**
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000701 * Toggle between different view modes: chat, diff, terminal
Sean McCullough86b56862025-04-18 13:04:03 -0700702 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100703 private toggleViewMode(mode: ViewMode, updateHistory: boolean): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700704 // Don't do anything if the mode is already active
705 if (this.viewMode === mode) return;
706
707 // Update the view mode
708 this.viewMode = mode;
709
710 if (updateHistory) {
711 // Update URL with the current view mode
712 this.updateUrlForViewMode(mode);
713 }
714
715 // Wait for DOM update to complete
716 this.updateComplete.then(() => {
717 // Update active view
Pokey Rule46fff972025-04-25 14:57:44 +0100718 const viewContainerInner = this.shadowRoot?.querySelector(
719 "#view-container-inner",
720 );
Sean McCullough86b56862025-04-18 13:04:03 -0700721 const chatView = this.shadowRoot?.querySelector(".chat-view");
722 const diffView = this.shadowRoot?.querySelector(".diff-view");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700723 const diff2View = this.shadowRoot?.querySelector(".diff2-view");
Sean McCullough86b56862025-04-18 13:04:03 -0700724 const terminalView = this.shadowRoot?.querySelector(".terminal-view");
725
726 // Remove active class from all views
727 chatView?.classList.remove("view-active");
728 diffView?.classList.remove("view-active");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700729 diff2View?.classList.remove("view-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700730 terminalView?.classList.remove("view-active");
731
732 // Add/remove diff-active class on view container
733 if (mode === "diff") {
Pokey Rule46fff972025-04-25 14:57:44 +0100734 viewContainerInner?.classList.add("diff-active");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700735 viewContainerInner?.classList.remove("diff2-active");
736 } else if (mode === "diff2") {
737 viewContainerInner?.classList.add("diff2-active");
738 viewContainerInner?.classList.remove("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700739 } else {
Pokey Rule46fff972025-04-25 14:57:44 +0100740 viewContainerInner?.classList.remove("diff-active");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700741 viewContainerInner?.classList.remove("diff2-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700742 }
743
744 // Add active class to the selected view
745 switch (mode) {
746 case "chat":
747 chatView?.classList.add("view-active");
748 break;
749 case "diff":
750 diffView?.classList.add("view-active");
751 // Load diff content if we have a diff view
752 const diffViewComp =
753 this.shadowRoot?.querySelector("sketch-diff-view");
754 if (diffViewComp && this.currentCommitHash) {
755 (diffViewComp as any).showCommitDiff(this.currentCommitHash);
756 } else if (diffViewComp) {
757 (diffViewComp as any).loadDiffContent();
758 }
759 break;
Autoformatter8c463622025-05-16 21:54:17 +0000760
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700761 case "diff2":
762 diff2View?.classList.add("view-active");
763 // Refresh git/recentlog when Monaco diff view is opened
764 // This ensures branch information is always up-to-date, as branches can change frequently
Autoformatter8c463622025-05-16 21:54:17 +0000765 const diff2ViewComp =
766 this.shadowRoot?.querySelector("sketch-diff2-view");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700767 if (diff2ViewComp) {
768 (diff2ViewComp as SketchDiff2View).refreshDiffView();
769 }
770 break;
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000771
Sean McCullough86b56862025-04-18 13:04:03 -0700772 case "terminal":
773 terminalView?.classList.add("view-active");
774 break;
775 }
776
777 // Update view mode buttons
778 const viewModeSelect = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700779 "sketch-view-mode-select",
Sean McCullough86b56862025-04-18 13:04:03 -0700780 );
781 if (viewModeSelect) {
782 const event = new CustomEvent("update-active-mode", {
783 detail: { mode },
784 bubbles: true,
785 composed: true,
786 });
787 viewModeSelect.dispatchEvent(event);
788 }
Sean McCullough86b56862025-04-18 13:04:03 -0700789 });
790 }
791
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000792 /**
793 * Updates the document title based on current title and connection status
794 */
795 private updateDocumentTitle(): void {
796 let docTitle = `sk: ${this.title || "untitled"}`;
797
798 // Add red circle emoji if disconnected
799 if (this.connectionStatus === "disconnected") {
800 docTitle += " 🔴";
801 }
802
803 document.title = docTitle;
804 }
805
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000806 // Check and request notification permission if needed
807 private async checkNotificationPermission(): Promise<boolean> {
808 // Check if the Notification API is supported
809 if (!("Notification" in window)) {
810 console.log("This browser does not support notifications");
811 return false;
812 }
813
814 // Check if permission is already granted
815 if (Notification.permission === "granted") {
816 return true;
817 }
818
819 // If permission is not denied, request it
820 if (Notification.permission !== "denied") {
821 const permission = await Notification.requestPermission();
822 return permission === "granted";
823 }
824
825 return false;
826 }
827
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000828 // Handle notifications toggle click
829 private _handleNotificationsToggle(): void {
830 this.notificationsEnabled = !this.notificationsEnabled;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000831
832 // If enabling notifications, check permissions
833 if (this.notificationsEnabled) {
834 this.checkNotificationPermission();
835 }
836
837 // Save preference to localStorage
838 try {
839 localStorage.setItem(
840 "sketch-notifications-enabled",
841 String(this.notificationsEnabled),
842 );
843 } catch (error) {
844 console.error("Error saving notification preference:", error);
845 }
846 }
847
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000848 // Handle window focus event
849 private _handleWindowFocus(): void {
850 this._windowFocused = true;
851 }
852
853 // Handle window blur event
854 private _handleWindowBlur(): void {
855 this._windowFocused = false;
856 }
857
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000858 // Show notification for message with EndOfTurn=true
859 private async showEndOfTurnNotification(
860 message: AgentMessage,
861 ): Promise<void> {
862 // Don't show notifications if they're disabled
863 if (!this.notificationsEnabled) return;
864
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000865 // Don't show notifications if the window is focused
866 if (this._windowFocused) return;
867
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000868 // Check if we have permission to show notifications
869 const hasPermission = await this.checkNotificationPermission();
870 if (!hasPermission) return;
871
Philip Zeyliger32011332025-04-30 20:59:40 +0000872 // Only show notifications for agent messages with end_of_turn=true and no parent_conversation_id
873 if (
874 message.type !== "agent" ||
875 !message.end_of_turn ||
876 message.parent_conversation_id
877 )
878 return;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000879
880 // Create a title that includes the sketch title
881 const notificationTitle = `Sketch: ${this.title || "untitled"}`;
882
883 // Extract the beginning of the message content (first 100 chars)
884 const messagePreview = message.content
885 ? message.content.substring(0, 100) +
886 (message.content.length > 100 ? "..." : "")
887 : "Agent has completed its turn";
888
889 // Create and show the notification
890 try {
891 new Notification(notificationTitle, {
892 body: messagePreview,
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000893 icon: "https://sketch.dev/favicon.ico", // Use sketch.dev favicon for notification
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000894 });
895 } catch (error) {
896 console.error("Error showing notification:", error);
897 }
898 }
899
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700900 // Check if todo panel should be visible based on latest todo content from messages or state
901 private checkTodoPanelVisibility(): void {
902 // Find the latest todo content from messages first
903 let latestTodoContent = "";
904 for (let i = this.messages.length - 1; i >= 0; i--) {
905 const message = this.messages[i];
906 if (message.todo_content !== undefined) {
907 latestTodoContent = message.todo_content || "";
908 break;
909 }
910 }
911
912 // If no todo content found in messages, check the current state
913 if (latestTodoContent === "" && this.containerState?.todo_content) {
914 latestTodoContent = this.containerState.todo_content;
915 }
916
917 // Parse the todo data to check if there are any actual todos
918 let hasTodos = false;
919 if (latestTodoContent.trim()) {
920 try {
921 const todoData = JSON.parse(latestTodoContent);
922 hasTodos = todoData.items && todoData.items.length > 0;
923 } catch (error) {
924 // Invalid JSON, treat as no todos
925 hasTodos = false;
926 }
927 }
928
929 this._todoPanelVisible = hasTodos;
930
931 // Update todo panel content if visible
932 if (hasTodos) {
933 const todoPanel = this.shadowRoot?.querySelector(
934 "sketch-todo-panel",
935 ) as any;
936 if (todoPanel && todoPanel.updateTodoContent) {
937 todoPanel.updateTodoContent(latestTodoContent);
938 }
939 }
940 }
941
Sean McCullough86b56862025-04-18 13:04:03 -0700942 private handleDataChanged(eventData: {
943 state: State;
Sean McCulloughd9f13372025-04-21 15:08:49 -0700944 newMessages: AgentMessage[];
Sean McCullough86b56862025-04-18 13:04:03 -0700945 }): void {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000946 const { state, newMessages } = eventData;
Sean McCullough86b56862025-04-18 13:04:03 -0700947
948 // Update state if we received it
949 if (state) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000950 // Ensure we're using the latest call status to prevent indicators from being stuck
Autoformatterf830c9d2025-04-30 18:16:01 +0000951 if (
952 state.outstanding_llm_calls === 0 &&
953 state.outstanding_tool_calls.length === 0
954 ) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000955 // Force reset containerState calls when nothing is reported as in progress
956 state.outstanding_llm_calls = 0;
957 state.outstanding_tool_calls = [];
958 }
Autoformatterf830c9d2025-04-30 18:16:01 +0000959
Sean McCullough86b56862025-04-18 13:04:03 -0700960 this.containerState = state;
961 this.title = state.title;
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000962
963 // Update document title when sketch title changes
964 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700965 }
966
Sean McCullough86b56862025-04-18 13:04:03 -0700967 // Update messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100968 this.messages = aggregateAgentMessages(this.messages, newMessages);
Autoformattercf570962025-04-30 17:27:39 +0000969
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000970 // Process new messages to find commit messages
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000971 // Update last commit info via container status component
972 if (this.containerStatusElement) {
973 this.containerStatusElement.updateLastCommitInfo(newMessages);
974 }
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000975
976 // Check for agent messages with end_of_turn=true and show notifications
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000977 if (newMessages && newMessages.length > 0) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000978 for (const message of newMessages) {
Philip Zeyliger32011332025-04-30 20:59:40 +0000979 if (
980 message.type === "agent" &&
981 message.end_of_turn &&
982 !message.parent_conversation_id
983 ) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000984 this.showEndOfTurnNotification(message);
985 break; // Only show one notification per batch of messages
986 }
987 }
988 }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700989
990 // Check if todo panel should be visible after agent loop iteration
991 this.checkTodoPanelVisibility();
992
993 // Ensure chat input observer is set up when new data comes in
994 if (!this.chatInputResizeObserver) {
995 this.setupChatInputObserver();
996 }
Sean McCullough86b56862025-04-18 13:04:03 -0700997 }
998
999 private handleConnectionStatusChanged(
1000 status: ConnectionStatus,
Philip Zeyliger72682df2025-04-23 13:09:46 -07001001 errorMessage?: string,
Sean McCullough86b56862025-04-18 13:04:03 -07001002 ): void {
1003 this.connectionStatus = status;
1004 this.connectionErrorMessage = errorMessage || "";
Philip Zeyliger9b999b02025-04-25 16:31:50 +00001005
1006 // Update document title when connection status changes
1007 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -07001008 }
1009
Sean McCulloughd3906e22025-04-29 17:32:14 +00001010 private async _handleStopClick(): Promise<void> {
1011 try {
1012 const response = await fetch("cancel", {
1013 method: "POST",
1014 headers: {
1015 "Content-Type": "application/json",
1016 },
1017 body: JSON.stringify({ reason: "user requested cancellation" }),
1018 });
1019
1020 if (!response.ok) {
1021 const errorData = await response.text();
1022 throw new Error(
1023 `Failed to stop operation: ${response.status} - ${errorData}`,
1024 );
1025 }
1026
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001027 // Stop request sent
Sean McCulloughd3906e22025-04-29 17:32:14 +00001028 } catch (error) {
1029 console.error("Error stopping operation:", error);
Sean McCulloughd3906e22025-04-29 17:32:14 +00001030 }
1031 }
1032
Pokey Rule397871d2025-05-19 15:02:45 +01001033 private async _handleEndClick(event?: Event): Promise<void> {
1034 if (event) {
1035 event.preventDefault();
1036 event.stopPropagation();
1037 }
1038 // Show confirmation dialog
1039 const confirmed = window.confirm(
1040 "Ending the session will shut down the underlying container. Are you sure?",
1041 );
1042 if (!confirmed) return;
1043
1044 try {
1045 const response = await fetch("end", {
1046 method: "POST",
1047 headers: {
1048 "Content-Type": "application/json",
1049 },
1050 body: JSON.stringify({ reason: "user requested end of session" }),
1051 });
1052
1053 if (!response.ok) {
1054 const errorData = await response.text();
1055 throw new Error(
1056 `Failed to end session: ${response.status} - ${errorData}`,
1057 );
1058 }
1059
1060 // After successful response, redirect to messages view
1061 // Extract the session ID from the URL
1062 const currentUrl = window.location.href;
1063 // The URL pattern should be like https://sketch.dev/s/cs71-8qa6-1124-aw79/
1064 const urlParts = currentUrl.split("/");
1065 let sessionId = "";
1066
1067 // Find the session ID in the URL (should be after /s/)
1068 for (let i = 0; i < urlParts.length; i++) {
1069 if (urlParts[i] === "s" && i + 1 < urlParts.length) {
1070 sessionId = urlParts[i + 1];
1071 break;
1072 }
1073 }
1074
1075 if (sessionId) {
1076 // Create the messages URL
1077 const messagesUrl = `/messages/${sessionId}`;
1078 // Redirect to messages view
1079 window.location.href = messagesUrl;
1080 }
1081
1082 // End request sent - connection will be closed by server
1083 } catch (error) {
1084 console.error("Error ending session:", error);
1085 }
1086 }
1087
Sean McCullough485afc62025-04-28 14:28:39 -07001088 async _handleMutlipleChoiceSelected(e: CustomEvent) {
1089 const chatInput = this.shadowRoot?.querySelector(
1090 "sketch-chat-input",
1091 ) as SketchChatInput;
1092 if (chatInput) {
1093 chatInput.content = e.detail.responseText;
1094 chatInput.focus();
1095 }
1096 }
1097
Sean McCullough86b56862025-04-18 13:04:03 -07001098 async _sendChat(e: CustomEvent) {
1099 console.log("app shell: _sendChat", e);
Sean McCullough485afc62025-04-28 14:28:39 -07001100 e.preventDefault();
1101 e.stopPropagation();
Sean McCullough86b56862025-04-18 13:04:03 -07001102 const message = e.detail.message?.trim();
1103 if (message == "") {
1104 return;
1105 }
1106 try {
Josh Bleecher Snyder98b64d12025-05-12 19:42:43 +00001107 // Always switch to chat view when sending a message so user can see processing
1108 if (this.viewMode !== "chat") {
1109 this.toggleViewMode("chat", true);
1110 }
Autoformatter5c7f9572025-05-13 01:17:31 +00001111
Sean McCullough86b56862025-04-18 13:04:03 -07001112 // Send the message to the server
1113 const response = await fetch("chat", {
1114 method: "POST",
1115 headers: {
1116 "Content-Type": "application/json",
1117 },
1118 body: JSON.stringify({ message }),
1119 });
1120
1121 if (!response.ok) {
1122 const errorData = await response.text();
1123 throw new Error(`Server error: ${response.status} - ${errorData}`);
1124 }
Sean McCullough86b56862025-04-18 13:04:03 -07001125 } catch (error) {
1126 console.error("Error sending chat message:", error);
1127 const statusText = document.getElementById("statusText");
1128 if (statusText) {
1129 statusText.textContent = "Error sending message";
1130 }
1131 }
1132 }
1133
Pokey Rule4097e532025-04-24 18:55:28 +01001134 private scrollContainerRef = createRef<HTMLElement>();
1135
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001136 /**
1137 * Set up ResizeObserver to monitor chat input height changes
1138 */
1139 private setupChatInputObserver(): void {
1140 // Wait for DOM to be ready
1141 this.updateComplete.then(() => {
1142 const chatInputElement = this.shadowRoot?.querySelector("#chat-input");
1143 if (chatInputElement && !this.chatInputResizeObserver) {
1144 this.chatInputResizeObserver = new ResizeObserver((entries) => {
1145 for (const entry of entries) {
1146 this.updateTodoPanelHeight(entry.contentRect.height);
1147 }
1148 });
1149
1150 this.chatInputResizeObserver.observe(chatInputElement);
1151
1152 // Initial height calculation
1153 const rect = chatInputElement.getBoundingClientRect();
1154 this.updateTodoPanelHeight(rect.height);
1155 }
1156 });
1157 }
1158
1159 /**
1160 * Update the CSS custom property that controls todo panel bottom position
1161 */
1162 private updateTodoPanelHeight(chatInputHeight: number): void {
1163 // Add some padding (20px) between todo panel and chat input
1164 const bottomOffset = chatInputHeight;
1165
1166 // Update the CSS custom property on the host element
1167 this.style.setProperty("--chat-input-height", `${bottomOffset}px`);
1168 }
1169
Sean McCullough86b56862025-04-18 13:04:03 -07001170 render() {
1171 return html`
Pokey Rule4097e532025-04-24 18:55:28 +01001172 <div id="top-banner">
Sean McCullough86b56862025-04-18 13:04:03 -07001173 <div class="title-container">
1174 <h1 class="banner-title">sketch</h1>
1175 <h2 id="chatTitle" class="chat-title">${this.title}</h2>
1176 </div>
1177
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001178 <!-- Container status info moved above tabs -->
Sean McCullough86b56862025-04-18 13:04:03 -07001179 <sketch-container-status
1180 .state=${this.containerState}
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001181 id="container-status"
Sean McCullough86b56862025-04-18 13:04:03 -07001182 ></sketch-container-status>
Autoformattercf570962025-04-30 17:27:39 +00001183
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001184 <!-- Last Commit section moved to sketch-container-status -->
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001185
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001186 <!-- Views section with tabs -->
1187 <sketch-view-mode-select></sketch-view-mode-select>
Sean McCullough86b56862025-04-18 13:04:03 -07001188
1189 <div class="refresh-control">
Sean McCulloughd3906e22025-04-29 17:32:14 +00001190 <button
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001191 id="stopButton"
1192 class="stop-button"
1193 ?disabled=${(this.containerState?.outstanding_llm_calls || 0) ===
1194 0 &&
1195 (this.containerState?.outstanding_tool_calls || []).length === 0}
1196 >
1197 <svg
1198 class="button-icon"
1199 xmlns="http://www.w3.org/2000/svg"
1200 viewBox="0 0 24 24"
1201 fill="none"
1202 stroke="currentColor"
1203 stroke-width="2"
1204 stroke-linecap="round"
1205 stroke-linejoin="round"
1206 >
1207 <rect x="6" y="6" width="12" height="12" />
1208 </svg>
1209 <span class="button-text">Stop</span>
Sean McCullough86b56862025-04-18 13:04:03 -07001210 </button>
Pokey Rule397871d2025-05-19 15:02:45 +01001211 <button
1212 id="endButton"
1213 class="end-button"
1214 @click=${this._handleEndClick}
1215 >
1216 <svg
1217 class="button-icon"
1218 xmlns="http://www.w3.org/2000/svg"
1219 viewBox="0 0 24 24"
1220 fill="none"
1221 stroke="currentColor"
1222 stroke-width="2"
1223 stroke-linecap="round"
1224 stroke-linejoin="round"
1225 >
1226 <path d="M18 6L6 18" />
1227 <path d="M6 6l12 12" />
1228 </svg>
1229 <span class="button-text">End</span>
1230 </button>
Sean McCullough86b56862025-04-18 13:04:03 -07001231
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +00001232 <div
1233 class="notifications-toggle"
1234 @click=${this._handleNotificationsToggle}
1235 title="${this.notificationsEnabled
1236 ? "Disable"
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001237 : "Enable"} notifications when the agent completes its turn"
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +00001238 >
1239 <div
1240 class="bell-icon ${!this.notificationsEnabled
1241 ? "bell-disabled"
1242 : ""}"
1243 >
1244 <!-- Bell SVG icon -->
1245 <svg
1246 xmlns="http://www.w3.org/2000/svg"
1247 width="16"
1248 height="16"
1249 fill="currentColor"
1250 viewBox="0 0 16 16"
1251 >
1252 <path
1253 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"
1254 />
1255 </svg>
1256 </div>
Philip Zeyligerbc6b6292025-04-30 18:00:15 +00001257 </div>
1258
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001259 <sketch-call-status
Sean McCulloughd9d45812025-04-30 16:53:41 -07001260 .agentState=${this.containerState?.agent_state}
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001261 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
1262 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
Philip Zeyliger72318392025-05-14 02:56:07 +00001263 .isIdle=${this.messages.length > 0
1264 ? this.messages[this.messages.length - 1]?.end_of_turn &&
1265 !this.messages[this.messages.length - 1]?.parent_conversation_id
1266 : true}
Philip Zeyliger5e357022025-05-16 04:50:34 +00001267 .isDisconnected=${this.connectionStatus === "disconnected"}
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001268 ></sketch-call-status>
Philip Zeyliger25f6ff12025-05-02 04:24:10 +00001269
1270 <sketch-network-status
1271 connection=${this.connectionStatus}
1272 error=${this.connectionErrorMessage}
1273 ></sketch-network-status>
Sean McCullough86b56862025-04-18 13:04:03 -07001274 </div>
1275 </div>
1276
Pokey Rule4097e532025-04-24 18:55:28 +01001277 <div id="view-container" ${ref(this.scrollContainerRef)}>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001278 <div
1279 id="view-container-inner"
1280 class="${this._todoPanelVisible && this.viewMode === "chat"
1281 ? "with-todo-panel"
1282 : ""}"
1283 >
Pokey Rule4097e532025-04-24 18:55:28 +01001284 <div
1285 class="chat-view ${this.viewMode === "chat" ? "view-active" : ""}"
1286 >
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001287 <div
1288 class="chat-timeline-container ${this._todoPanelVisible &&
1289 this.viewMode === "chat"
1290 ? "with-todo-panel"
1291 : ""}"
1292 >
1293 <sketch-timeline
1294 .messages=${this.messages}
1295 .scrollContainer=${this.scrollContainerRef}
1296 .agentState=${this.containerState?.agent_state}
1297 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
1298 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
1299 ></sketch-timeline>
1300 </div>
1301 </div>
1302
1303 <!-- Todo panel positioned outside the main flow - only visible in chat view -->
1304 <div
1305 class="todo-panel-container ${this._todoPanelVisible &&
1306 this.viewMode === "chat"
1307 ? "visible"
1308 : ""}"
1309 >
1310 <sketch-todo-panel
1311 .visible=${this._todoPanelVisible && this.viewMode === "chat"}
1312 ></sketch-todo-panel>
Pokey Rule4097e532025-04-24 18:55:28 +01001313 </div>
1314 <div
1315 class="diff-view ${this.viewMode === "diff" ? "view-active" : ""}"
1316 >
1317 <sketch-diff-view
1318 .commitHash=${this.currentCommitHash}
1319 ></sketch-diff-view>
1320 </div>
Autoformatter8c463622025-05-16 21:54:17 +00001321
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001322 <div
1323 class="diff2-view ${this.viewMode === "diff2" ? "view-active" : ""}"
1324 >
1325 <sketch-diff2-view
1326 .commit=${this.currentCommitHash}
1327 .gitService=${new DefaultGitDataService()}
1328 @diff-comment="${this._handleDiffComment}"
1329 ></sketch-diff2-view>
1330 </div>
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +00001331
Pokey Rule4097e532025-04-24 18:55:28 +01001332 <div
1333 class="terminal-view ${this.viewMode === "terminal"
1334 ? "view-active"
1335 : ""}"
1336 >
1337 <sketch-terminal></sketch-terminal>
1338 </div>
Sean McCullough86b56862025-04-18 13:04:03 -07001339 </div>
1340 </div>
1341
Pokey Rule4097e532025-04-24 18:55:28 +01001342 <div id="chat-input">
1343 <sketch-chat-input @send-chat="${this._sendChat}"></sketch-chat-input>
1344 </div>
Sean McCullough86b56862025-04-18 13:04:03 -07001345 `;
1346 }
1347
1348 /**
Sean McCullough86b56862025-04-18 13:04:03 -07001349 * Lifecycle callback when component is first connected to DOM
1350 */
1351 firstUpdated(): void {
1352 if (this.viewMode !== "chat") {
1353 return;
1354 }
1355
1356 // Initial scroll to bottom when component is first rendered
1357 setTimeout(
1358 () => this.scrollTo({ top: this.scrollHeight, behavior: "smooth" }),
Philip Zeyliger72682df2025-04-23 13:09:46 -07001359 50,
Sean McCullough86b56862025-04-18 13:04:03 -07001360 );
1361
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001362 // Setup stop button
1363 const stopButton = this.renderRoot?.querySelector(
1364 "#stopButton",
1365 ) as HTMLButtonElement;
1366 stopButton?.addEventListener("click", async () => {
1367 try {
Sean McCullough495cb962025-05-01 16:25:53 -07001368 const response = await fetch("cancel", {
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001369 method: "POST",
1370 headers: {
1371 "Content-Type": "application/json",
1372 },
1373 body: JSON.stringify({ reason: "User clicked stop button" }),
1374 });
1375 if (!response.ok) {
1376 console.error("Failed to cancel:", await response.text());
1377 }
1378 } catch (error) {
1379 console.error("Error cancelling operation:", error);
1380 }
1381 });
1382
Pokey Rule397871d2025-05-19 15:02:45 +01001383 // Setup end button
1384 const endButton = this.renderRoot?.querySelector(
1385 "#endButton",
1386 ) as HTMLButtonElement;
1387 // We're already using the @click binding in the HTML, so manual event listener not needed here
1388
Philip Zeyliger47b71c92025-04-30 15:43:39 +00001389 // Process any existing messages to find commit information
1390 if (this.messages && this.messages.length > 0) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001391 // Update last commit info via container status component
1392 if (this.containerStatusElement) {
1393 this.containerStatusElement.updateLastCommitInfo(this.messages);
1394 }
Philip Zeyliger47b71c92025-04-30 15:43:39 +00001395 }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001396
1397 // Set up chat input height observer for todo panel
1398 this.setupChatInputObserver();
Sean McCullough86b56862025-04-18 13:04:03 -07001399 }
1400}
1401
1402declare global {
1403 interface HTMLElementTagNameMap {
1404 "sketch-app-shell": SketchAppShell;
1405 }
1406}