blob: d65093c70fd541cd596b7fb5fb509cb3fbba3b31 [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";
20
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 Zeyliger272a90e2025-05-16 14:49:51 -070024type ViewMode = "chat" | "diff" | "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
142 #chat-input {
143 align-self: flex-end;
144 width: 100%;
145 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
146 }
147
Sean McCullough86b56862025-04-18 13:04:03 -0700148 .banner-title {
149 font-size: 18px;
150 font-weight: 600;
151 margin: 0;
152 min-width: 6em;
153 white-space: nowrap;
154 overflow: hidden;
155 text-overflow: ellipsis;
156 }
157
158 .chat-title {
159 margin: 0;
160 padding: 0;
161 color: rgba(82, 82, 82, 0.85);
Josh Bleecher Snydereb5166a2025-04-30 17:04:20 +0000162 font-size: 14px;
Sean McCullough86b56862025-04-18 13:04:03 -0700163 font-weight: normal;
164 font-style: italic;
165 white-space: nowrap;
166 overflow: hidden;
167 text-overflow: ellipsis;
168 }
169
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700170 /* Allow the container to expand to full width and height in diff mode */
171 #view-container-inner.diff-active,
172 #view-container-inner.diff2-active {
Sean McCullough86b56862025-04-18 13:04:03 -0700173 max-width: 100%;
Pokey Rule46fff972025-04-25 14:57:44 +0100174 width: 100%;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700175 height: 100%;
176 padding: 0; /* Remove padding for more space */
177 display: flex;
178 flex-direction: column;
179 flex: 1;
180 min-height: 0; /* Critical for flex behavior */
Sean McCullough86b56862025-04-18 13:04:03 -0700181 }
182
183 /* Individual view styles */
184 .chat-view,
185 .diff-view,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700186 .diff2-view,
Sean McCullough86b56862025-04-18 13:04:03 -0700187 .terminal-view {
188 display: none; /* Hidden by default */
189 width: 100%;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700190 height: 100%;
191 }
Autoformatter8c463622025-05-16 21:54:17 +0000192
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700193 /* Make chat view take full width available */
194 .chat-view.view-active {
195 display: flex;
196 flex-direction: column;
197 width: 100%;
198 }
Autoformatter8c463622025-05-16 21:54:17 +0000199
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700200 /* Monaco diff2 view needs to take all available space */
201 .diff2-view.view-active {
202 flex: 1;
203 overflow: hidden;
204 min-height: 0; /* Required for proper flex child behavior */
205 display: flex;
206 flex-direction: column;
207 height: 100%;
Sean McCullough86b56862025-04-18 13:04:03 -0700208 }
209
210 /* Active view styles - these will be applied via JavaScript */
211 .view-active {
212 display: flex;
213 flex-direction: column;
214 }
215
216 .title-container {
217 display: flex;
218 flex-direction: column;
219 white-space: nowrap;
220 overflow: hidden;
221 text-overflow: ellipsis;
Josh Bleecher Snydereb5166a2025-04-30 17:04:20 +0000222 max-width: 30%;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000223 padding: 6px 0;
Sean McCullough86b56862025-04-18 13:04:03 -0700224 }
225
226 .refresh-control {
227 display: flex;
228 align-items: center;
229 margin-bottom: 0;
230 flex-wrap: nowrap;
231 white-space: nowrap;
232 flex-shrink: 0;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000233 gap: 15px;
234 padding-left: 15px;
235 margin-right: 50px;
Sean McCullough86b56862025-04-18 13:04:03 -0700236 }
237
Pokey Rule397871d2025-05-19 15:02:45 +0100238 .stop-button,
239 .end-button {
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700240 background: #2196f3;
241 color: white;
242 border: none;
243 padding: 4px 10px;
244 border-radius: 4px;
245 cursor: pointer;
246 font-size: 12px;
247 margin-right: 5px;
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000248 display: flex;
249 align-items: center;
250 gap: 6px;
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700251 }
252
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000253 .stop-button {
254 background: #dc3545;
Sean McCullough86b56862025-04-18 13:04:03 -0700255 color: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700256 }
257
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000258 .stop-button:hover:not(:disabled) {
259 background-color: #c82333;
Sean McCullough86b56862025-04-18 13:04:03 -0700260 }
261
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000262 .stop-button:disabled {
263 background-color: #e9a8ad;
264 cursor: not-allowed;
265 opacity: 0.7;
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000266 }
267
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000268 .stop-button:disabled:hover {
269 background-color: #e9a8ad;
270 }
271
Pokey Rule397871d2025-05-19 15:02:45 +0100272 .end-button {
273 background: #6c757d;
274 color: white;
275 }
276
277 .end-button:hover:not(:disabled) {
278 background-color: #5a6268;
279 }
280
281 .end-button:disabled {
282 background-color: #a9acaf;
283 cursor: not-allowed;
284 opacity: 0.7;
285 }
286
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000287 .button-icon {
288 width: 16px;
289 height: 16px;
290 }
291
292 @media (max-width: 1400px) {
293 .button-text {
294 display: none;
295 }
296
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000297 .stop-button {
298 padding: 6px;
299 }
300 }
301
302 /* Removed poll-updates class */
303
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000304 .notifications-toggle {
Sean McCullough86b56862025-04-18 13:04:03 -0700305 display: flex;
306 align-items: center;
Sean McCullough86b56862025-04-18 13:04:03 -0700307 font-size: 12px;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000308 margin-right: 10px;
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000309 cursor: pointer;
310 }
311
312 .bell-icon {
313 width: 20px;
314 height: 20px;
315 position: relative;
316 display: inline-flex;
317 align-items: center;
318 justify-content: center;
319 }
320
321 .bell-disabled::before {
322 content: "";
323 position: absolute;
324 width: 2px;
325 height: 24px;
326 background-color: #dc3545;
327 transform: rotate(45deg);
328 transform-origin: center center;
Sean McCullough86b56862025-04-18 13:04:03 -0700329 }
330 `;
331
332 // Header bar: Network connection status details
333 @property()
334 connectionStatus: ConnectionStatus = "disconnected";
Autoformattercf570962025-04-30 17:27:39 +0000335
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000336 // Track if the last commit info has been copied
337 @state()
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000338 // lastCommitCopied moved to sketch-container-status
Sean McCullough86b56862025-04-18 13:04:03 -0700339
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000340 // Track notification preferences
341 @state()
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000342 notificationsEnabled: boolean = false;
343
344 // Track if the window is focused to control notifications
345 @state()
346 private _windowFocused: boolean = document.hasFocus();
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000347
Sean McCullough86b56862025-04-18 13:04:03 -0700348 @property()
349 connectionErrorMessage: string = "";
350
Sean McCullough86b56862025-04-18 13:04:03 -0700351 // Chat messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100352 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700353 messages: AgentMessage[] = [];
Sean McCullough86b56862025-04-18 13:04:03 -0700354
355 @property()
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000356 set title(value: string) {
357 const oldValue = this._title;
358 this._title = value;
359 this.requestUpdate("title", oldValue);
360 // Update document title when title property changes
361 this.updateDocumentTitle();
362 }
363
364 get title(): string {
365 return this._title;
366 }
367
368 private _title: string = "";
Sean McCullough86b56862025-04-18 13:04:03 -0700369
370 private dataManager = new DataManager();
371
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100372 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700373 containerState: State = {
Philip Zeyligerd03318d2025-05-08 13:09:12 -0700374 state_version: 2,
Sean McCulloughd9f13372025-04-21 15:08:49 -0700375 title: "",
376 os: "",
377 message_count: 0,
378 hostname: "",
379 working_dir: "",
380 initial_commit: "",
Philip Zeyliger99a9a022025-04-27 15:15:25 +0000381 outstanding_llm_calls: 0,
382 outstanding_tool_calls: [],
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000383 session_id: "",
384 ssh_available: false,
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700385 ssh_error: "",
386 in_container: false,
387 first_message_index: 0,
Sean McCulloughd9f13372025-04-21 15:08:49 -0700388 };
Sean McCullough86b56862025-04-18 13:04:03 -0700389
Philip Zeyliger14fe75d2025-05-22 17:39:38 +0000390
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700391
Sean McCullough86b56862025-04-18 13:04:03 -0700392 // Mutation observer to detect when new messages are added
393 private mutationObserver: MutationObserver | null = null;
394
395 constructor() {
396 super();
397
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000398 // Reference to the container status element
399 this.containerStatusElement = null;
400
Sean McCullough86b56862025-04-18 13:04:03 -0700401 // Binding methods to this
402 this._handleViewModeSelect = this._handleViewModeSelect.bind(this);
Sean McCullough34bb09a2025-05-13 15:39:54 -0700403 this._handlePopState = this._handlePopState.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -0700404 this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
Sean McCullough485afc62025-04-28 14:28:39 -0700405 this._handleMutlipleChoiceSelected =
406 this._handleMutlipleChoiceSelected.bind(this);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000407 this._handleStopClick = this._handleStopClick.bind(this);
Pokey Rule397871d2025-05-19 15:02:45 +0100408 this._handleEndClick = this._handleEndClick.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000409 this._handleNotificationsToggle =
410 this._handleNotificationsToggle.bind(this);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000411 this._handleWindowFocus = this._handleWindowFocus.bind(this);
412 this._handleWindowBlur = this._handleWindowBlur.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000413
414 // Load notification preference from localStorage
415 try {
416 const savedPref = localStorage.getItem("sketch-notifications-enabled");
417 if (savedPref !== null) {
418 this.notificationsEnabled = savedPref === "true";
419 }
420 } catch (error) {
421 console.error("Error loading notification preference:", error);
422 }
Sean McCullough86b56862025-04-18 13:04:03 -0700423 }
424
425 // See https://lit.dev/docs/components/lifecycle/
426 connectedCallback() {
427 super.connectedCallback();
428
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000429 // Get reference to the container status element
430 setTimeout(() => {
431 this.containerStatusElement =
432 this.shadowRoot?.getElementById("container-status");
433 }, 0);
434
Sean McCullough86b56862025-04-18 13:04:03 -0700435 // Initialize client-side nav history.
436 const url = new URL(window.location.href);
437 const mode = url.searchParams.get("view") || "chat";
438 window.history.replaceState({ mode }, "", url.toString());
439
440 this.toggleViewMode(mode as ViewMode, false);
441 // Add popstate event listener to handle browser back/forward navigation
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100442 window.addEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700443
444 // Add event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100445 window.addEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100446 window.addEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700447
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000448 // Add window focus/blur listeners for controlling notifications
449 window.addEventListener("focus", this._handleWindowFocus);
450 window.addEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700451 window.addEventListener(
452 "multiple-choice-selected",
453 this._handleMutlipleChoiceSelected,
454 );
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000455
Sean McCullough86b56862025-04-18 13:04:03 -0700456 // register event listeners
457 this.dataManager.addEventListener(
458 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700459 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700460 );
461 this.dataManager.addEventListener(
462 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700463 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700464 );
465
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000466 // Set initial document title
467 this.updateDocumentTitle();
468
Sean McCullough86b56862025-04-18 13:04:03 -0700469 // Initialize the data manager
470 this.dataManager.initialize();
Autoformattercf570962025-04-30 17:27:39 +0000471
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000472 // Process existing messages for commit info
473 if (this.messages && this.messages.length > 0) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000474 // Update last commit info via container status component
475 setTimeout(() => {
476 if (this.containerStatusElement) {
477 this.containerStatusElement.updateLastCommitInfo(this.messages);
478 }
479 }, 100);
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000480 }
Sean McCullough86b56862025-04-18 13:04:03 -0700481 }
482
483 // See https://lit.dev/docs/components/lifecycle/
484 disconnectedCallback() {
485 super.disconnectedCallback();
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100486 window.removeEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700487
488 // Remove event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100489 window.removeEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100490 window.removeEventListener("show-commit-diff", this._handleShowCommitDiff);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000491 window.removeEventListener("focus", this._handleWindowFocus);
492 window.removeEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700493 window.removeEventListener(
494 "multiple-choice-selected",
495 this._handleMutlipleChoiceSelected,
496 );
Sean McCullough86b56862025-04-18 13:04:03 -0700497
498 // unregister data manager event listeners
499 this.dataManager.removeEventListener(
500 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700501 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700502 );
503 this.dataManager.removeEventListener(
504 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700505 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700506 );
507
508 // Disconnect mutation observer if it exists
509 if (this.mutationObserver) {
Sean McCullough86b56862025-04-18 13:04:03 -0700510 this.mutationObserver.disconnect();
511 this.mutationObserver = null;
512 }
513 }
514
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700515 updateUrlForViewMode(mode: ViewMode): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700516 // Get the current URL without search parameters
517 const url = new URL(window.location.href);
518
519 // Clear existing parameters
520 url.search = "";
521
522 // Only add view parameter if not in default chat view
523 if (mode !== "chat") {
524 url.searchParams.set("view", mode);
Sean McCullough71941bd2025-04-18 13:31:48 -0700525 const diffView = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700526 ".diff-view",
Sean McCullough71941bd2025-04-18 13:31:48 -0700527 ) as SketchDiffView;
Sean McCullough86b56862025-04-18 13:04:03 -0700528
529 // If in diff view and there's a commit hash, include that too
530 if (mode === "diff" && diffView.commitHash) {
531 url.searchParams.set("commit", diffView.commitHash);
532 }
533 }
534
535 // Update the browser history without reloading the page
536 window.history.pushState({ mode }, "", url.toString());
537 }
538
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100539 private _handlePopState(event: PopStateEvent) {
Sean McCullough86b56862025-04-18 13:04:03 -0700540 if (event.state && event.state.mode) {
541 this.toggleViewMode(event.state.mode, false);
542 } else {
543 this.toggleViewMode("chat", false);
544 }
545 }
546
547 /**
548 * Handle view mode selection event
549 */
550 private _handleViewModeSelect(event: CustomEvent) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700551 const mode = event.detail.mode as "chat" | "diff" | "diff2" | "terminal";
Sean McCullough86b56862025-04-18 13:04:03 -0700552 this.toggleViewMode(mode, true);
553 }
554
555 /**
556 * Handle show commit diff event
557 */
558 private _handleShowCommitDiff(event: CustomEvent) {
559 const { commitHash } = event.detail;
560 if (commitHash) {
561 this.showCommitDiff(commitHash);
562 }
563 }
564
Sean McCullough485afc62025-04-28 14:28:39 -0700565 private _handleMultipleChoice(event: CustomEvent) {
566 window.console.log("_handleMultipleChoice", event);
567 this._sendChat;
568 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700569
570 private _handleDiffComment(event: CustomEvent) {
571 // Empty stub required by the event binding in the template
572 // Actual handling occurs at global level in sketch-chat-input component
573 }
Sean McCullough86b56862025-04-18 13:04:03 -0700574 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700575 * Listen for commit diff event
576 * @param commitHash The commit hash to show diff for
577 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100578 private showCommitDiff(commitHash: string): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700579 // Store the commit hash
580 this.currentCommitHash = commitHash;
581
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700582 this.toggleViewMode("diff2", true);
Sean McCullough86b56862025-04-18 13:04:03 -0700583
Sean McCullough86b56862025-04-18 13:04:03 -0700584 this.updateComplete.then(() => {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700585 const diff2View = this.shadowRoot?.querySelector("sketch-diff2-view");
586 if (diff2View) {
587 (diff2View as SketchDiff2View).refreshDiffView();
Sean McCullough86b56862025-04-18 13:04:03 -0700588 }
589 });
590 }
591
592 /**
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000593 * Toggle between different view modes: chat, diff, terminal
Sean McCullough86b56862025-04-18 13:04:03 -0700594 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100595 private toggleViewMode(mode: ViewMode, updateHistory: boolean): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700596 // Don't do anything if the mode is already active
597 if (this.viewMode === mode) return;
598
599 // Update the view mode
600 this.viewMode = mode;
601
602 if (updateHistory) {
603 // Update URL with the current view mode
604 this.updateUrlForViewMode(mode);
605 }
606
607 // Wait for DOM update to complete
608 this.updateComplete.then(() => {
609 // Update active view
Pokey Rule46fff972025-04-25 14:57:44 +0100610 const viewContainerInner = this.shadowRoot?.querySelector(
611 "#view-container-inner",
612 );
Sean McCullough86b56862025-04-18 13:04:03 -0700613 const chatView = this.shadowRoot?.querySelector(".chat-view");
614 const diffView = this.shadowRoot?.querySelector(".diff-view");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700615 const diff2View = this.shadowRoot?.querySelector(".diff2-view");
Sean McCullough86b56862025-04-18 13:04:03 -0700616 const terminalView = this.shadowRoot?.querySelector(".terminal-view");
617
618 // Remove active class from all views
619 chatView?.classList.remove("view-active");
620 diffView?.classList.remove("view-active");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700621 diff2View?.classList.remove("view-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700622 terminalView?.classList.remove("view-active");
623
624 // Add/remove diff-active class on view container
625 if (mode === "diff") {
Pokey Rule46fff972025-04-25 14:57:44 +0100626 viewContainerInner?.classList.add("diff-active");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700627 viewContainerInner?.classList.remove("diff2-active");
628 } else if (mode === "diff2") {
629 viewContainerInner?.classList.add("diff2-active");
630 viewContainerInner?.classList.remove("diff-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700631 } else {
Pokey Rule46fff972025-04-25 14:57:44 +0100632 viewContainerInner?.classList.remove("diff-active");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700633 viewContainerInner?.classList.remove("diff2-active");
Sean McCullough86b56862025-04-18 13:04:03 -0700634 }
635
636 // Add active class to the selected view
637 switch (mode) {
638 case "chat":
639 chatView?.classList.add("view-active");
640 break;
641 case "diff":
642 diffView?.classList.add("view-active");
643 // Load diff content if we have a diff view
644 const diffViewComp =
645 this.shadowRoot?.querySelector("sketch-diff-view");
646 if (diffViewComp && this.currentCommitHash) {
647 (diffViewComp as any).showCommitDiff(this.currentCommitHash);
648 } else if (diffViewComp) {
649 (diffViewComp as any).loadDiffContent();
650 }
651 break;
Autoformatter8c463622025-05-16 21:54:17 +0000652
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700653 case "diff2":
654 diff2View?.classList.add("view-active");
655 // Refresh git/recentlog when Monaco diff view is opened
656 // This ensures branch information is always up-to-date, as branches can change frequently
Autoformatter8c463622025-05-16 21:54:17 +0000657 const diff2ViewComp =
658 this.shadowRoot?.querySelector("sketch-diff2-view");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700659 if (diff2ViewComp) {
660 (diff2ViewComp as SketchDiff2View).refreshDiffView();
661 }
662 break;
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +0000663
Sean McCullough86b56862025-04-18 13:04:03 -0700664 case "terminal":
665 terminalView?.classList.add("view-active");
666 break;
667 }
668
669 // Update view mode buttons
670 const viewModeSelect = this.shadowRoot?.querySelector(
Philip Zeyliger72682df2025-04-23 13:09:46 -0700671 "sketch-view-mode-select",
Sean McCullough86b56862025-04-18 13:04:03 -0700672 );
673 if (viewModeSelect) {
674 const event = new CustomEvent("update-active-mode", {
675 detail: { mode },
676 bubbles: true,
677 composed: true,
678 });
679 viewModeSelect.dispatchEvent(event);
680 }
Sean McCullough86b56862025-04-18 13:04:03 -0700681 });
682 }
683
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000684 /**
685 * Updates the document title based on current title and connection status
686 */
687 private updateDocumentTitle(): void {
688 let docTitle = `sk: ${this.title || "untitled"}`;
689
690 // Add red circle emoji if disconnected
691 if (this.connectionStatus === "disconnected") {
692 docTitle += " 🔴";
693 }
694
695 document.title = docTitle;
696 }
697
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000698 // Check and request notification permission if needed
699 private async checkNotificationPermission(): Promise<boolean> {
700 // Check if the Notification API is supported
701 if (!("Notification" in window)) {
702 console.log("This browser does not support notifications");
703 return false;
704 }
705
706 // Check if permission is already granted
707 if (Notification.permission === "granted") {
708 return true;
709 }
710
711 // If permission is not denied, request it
712 if (Notification.permission !== "denied") {
713 const permission = await Notification.requestPermission();
714 return permission === "granted";
715 }
716
717 return false;
718 }
719
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000720 // Handle notifications toggle click
721 private _handleNotificationsToggle(): void {
722 this.notificationsEnabled = !this.notificationsEnabled;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000723
724 // If enabling notifications, check permissions
725 if (this.notificationsEnabled) {
726 this.checkNotificationPermission();
727 }
728
729 // Save preference to localStorage
730 try {
731 localStorage.setItem(
732 "sketch-notifications-enabled",
733 String(this.notificationsEnabled),
734 );
735 } catch (error) {
736 console.error("Error saving notification preference:", error);
737 }
738 }
739
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000740 // Handle window focus event
741 private _handleWindowFocus(): void {
742 this._windowFocused = true;
743 }
744
745 // Handle window blur event
746 private _handleWindowBlur(): void {
747 this._windowFocused = false;
748 }
749
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000750 // Show notification for message with EndOfTurn=true
751 private async showEndOfTurnNotification(
752 message: AgentMessage,
753 ): Promise<void> {
754 // Don't show notifications if they're disabled
755 if (!this.notificationsEnabled) return;
756
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000757 // Don't show notifications if the window is focused
758 if (this._windowFocused) return;
759
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000760 // Check if we have permission to show notifications
761 const hasPermission = await this.checkNotificationPermission();
762 if (!hasPermission) return;
763
Philip Zeyliger32011332025-04-30 20:59:40 +0000764 // Only show notifications for agent messages with end_of_turn=true and no parent_conversation_id
765 if (
766 message.type !== "agent" ||
767 !message.end_of_turn ||
768 message.parent_conversation_id
769 )
770 return;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000771
772 // Create a title that includes the sketch title
773 const notificationTitle = `Sketch: ${this.title || "untitled"}`;
774
775 // Extract the beginning of the message content (first 100 chars)
776 const messagePreview = message.content
777 ? message.content.substring(0, 100) +
778 (message.content.length > 100 ? "..." : "")
779 : "Agent has completed its turn";
780
781 // Create and show the notification
782 try {
783 new Notification(notificationTitle, {
784 body: messagePreview,
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000785 icon: "https://sketch.dev/favicon.ico", // Use sketch.dev favicon for notification
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000786 });
787 } catch (error) {
788 console.error("Error showing notification:", error);
789 }
790 }
791
Sean McCullough86b56862025-04-18 13:04:03 -0700792 private handleDataChanged(eventData: {
793 state: State;
Sean McCulloughd9f13372025-04-21 15:08:49 -0700794 newMessages: AgentMessage[];
Sean McCullough86b56862025-04-18 13:04:03 -0700795 }): void {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000796 const { state, newMessages } = eventData;
Sean McCullough86b56862025-04-18 13:04:03 -0700797
798 // Update state if we received it
799 if (state) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000800 // Ensure we're using the latest call status to prevent indicators from being stuck
Autoformatterf830c9d2025-04-30 18:16:01 +0000801 if (
802 state.outstanding_llm_calls === 0 &&
803 state.outstanding_tool_calls.length === 0
804 ) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000805 // Force reset containerState calls when nothing is reported as in progress
806 state.outstanding_llm_calls = 0;
807 state.outstanding_tool_calls = [];
808 }
Autoformatterf830c9d2025-04-30 18:16:01 +0000809
Sean McCullough86b56862025-04-18 13:04:03 -0700810 this.containerState = state;
811 this.title = state.title;
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000812
813 // Update document title when sketch title changes
814 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700815 }
816
Sean McCullough86b56862025-04-18 13:04:03 -0700817 // Update messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100818 this.messages = aggregateAgentMessages(this.messages, newMessages);
Autoformattercf570962025-04-30 17:27:39 +0000819
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000820 // Process new messages to find commit messages
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000821 // Update last commit info via container status component
822 if (this.containerStatusElement) {
823 this.containerStatusElement.updateLastCommitInfo(newMessages);
824 }
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000825
826 // Check for agent messages with end_of_turn=true and show notifications
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000827 if (newMessages && newMessages.length > 0) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000828 for (const message of newMessages) {
Philip Zeyliger32011332025-04-30 20:59:40 +0000829 if (
830 message.type === "agent" &&
831 message.end_of_turn &&
832 !message.parent_conversation_id
833 ) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000834 this.showEndOfTurnNotification(message);
835 break; // Only show one notification per batch of messages
836 }
837 }
838 }
Sean McCullough86b56862025-04-18 13:04:03 -0700839 }
840
841 private handleConnectionStatusChanged(
842 status: ConnectionStatus,
Philip Zeyliger72682df2025-04-23 13:09:46 -0700843 errorMessage?: string,
Sean McCullough86b56862025-04-18 13:04:03 -0700844 ): void {
845 this.connectionStatus = status;
846 this.connectionErrorMessage = errorMessage || "";
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000847
848 // Update document title when connection status changes
849 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700850 }
851
Sean McCulloughd3906e22025-04-29 17:32:14 +0000852 private async _handleStopClick(): Promise<void> {
853 try {
854 const response = await fetch("cancel", {
855 method: "POST",
856 headers: {
857 "Content-Type": "application/json",
858 },
859 body: JSON.stringify({ reason: "user requested cancellation" }),
860 });
861
862 if (!response.ok) {
863 const errorData = await response.text();
864 throw new Error(
865 `Failed to stop operation: ${response.status} - ${errorData}`,
866 );
867 }
868
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000869 // Stop request sent
Sean McCulloughd3906e22025-04-29 17:32:14 +0000870 } catch (error) {
871 console.error("Error stopping operation:", error);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000872 }
873 }
874
Pokey Rule397871d2025-05-19 15:02:45 +0100875 private async _handleEndClick(event?: Event): Promise<void> {
876 if (event) {
877 event.preventDefault();
878 event.stopPropagation();
879 }
880 // Show confirmation dialog
881 const confirmed = window.confirm(
882 "Ending the session will shut down the underlying container. Are you sure?",
883 );
884 if (!confirmed) return;
885
886 try {
887 const response = await fetch("end", {
888 method: "POST",
889 headers: {
890 "Content-Type": "application/json",
891 },
892 body: JSON.stringify({ reason: "user requested end of session" }),
893 });
894
895 if (!response.ok) {
896 const errorData = await response.text();
897 throw new Error(
898 `Failed to end session: ${response.status} - ${errorData}`,
899 );
900 }
901
902 // After successful response, redirect to messages view
903 // Extract the session ID from the URL
904 const currentUrl = window.location.href;
905 // The URL pattern should be like https://sketch.dev/s/cs71-8qa6-1124-aw79/
906 const urlParts = currentUrl.split("/");
907 let sessionId = "";
908
909 // Find the session ID in the URL (should be after /s/)
910 for (let i = 0; i < urlParts.length; i++) {
911 if (urlParts[i] === "s" && i + 1 < urlParts.length) {
912 sessionId = urlParts[i + 1];
913 break;
914 }
915 }
916
917 if (sessionId) {
918 // Create the messages URL
919 const messagesUrl = `/messages/${sessionId}`;
920 // Redirect to messages view
921 window.location.href = messagesUrl;
922 }
923
924 // End request sent - connection will be closed by server
925 } catch (error) {
926 console.error("Error ending session:", error);
927 }
928 }
929
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700930
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700931
Sean McCullough485afc62025-04-28 14:28:39 -0700932 async _handleMutlipleChoiceSelected(e: CustomEvent) {
933 const chatInput = this.shadowRoot?.querySelector(
934 "sketch-chat-input",
935 ) as SketchChatInput;
936 if (chatInput) {
937 chatInput.content = e.detail.responseText;
938 chatInput.focus();
939 }
940 }
941
Sean McCullough86b56862025-04-18 13:04:03 -0700942 async _sendChat(e: CustomEvent) {
943 console.log("app shell: _sendChat", e);
Sean McCullough485afc62025-04-28 14:28:39 -0700944 e.preventDefault();
945 e.stopPropagation();
Sean McCullough86b56862025-04-18 13:04:03 -0700946 const message = e.detail.message?.trim();
947 if (message == "") {
948 return;
949 }
950 try {
Josh Bleecher Snyder98b64d12025-05-12 19:42:43 +0000951 // Always switch to chat view when sending a message so user can see processing
952 if (this.viewMode !== "chat") {
953 this.toggleViewMode("chat", true);
954 }
Autoformatter5c7f9572025-05-13 01:17:31 +0000955
Sean McCullough86b56862025-04-18 13:04:03 -0700956 // Send the message to the server
957 const response = await fetch("chat", {
958 method: "POST",
959 headers: {
960 "Content-Type": "application/json",
961 },
962 body: JSON.stringify({ message }),
963 });
964
965 if (!response.ok) {
966 const errorData = await response.text();
967 throw new Error(`Server error: ${response.status} - ${errorData}`);
968 }
Sean McCullough86b56862025-04-18 13:04:03 -0700969 } catch (error) {
970 console.error("Error sending chat message:", error);
971 const statusText = document.getElementById("statusText");
972 if (statusText) {
973 statusText.textContent = "Error sending message";
974 }
975 }
976 }
977
Pokey Rule4097e532025-04-24 18:55:28 +0100978 private scrollContainerRef = createRef<HTMLElement>();
979
Sean McCullough86b56862025-04-18 13:04:03 -0700980 render() {
981 return html`
Pokey Rule4097e532025-04-24 18:55:28 +0100982 <div id="top-banner">
Sean McCullough86b56862025-04-18 13:04:03 -0700983 <div class="title-container">
984 <h1 class="banner-title">sketch</h1>
985 <h2 id="chatTitle" class="chat-title">${this.title}</h2>
986 </div>
987
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000988 <!-- Container status info moved above tabs -->
Sean McCullough86b56862025-04-18 13:04:03 -0700989 <sketch-container-status
990 .state=${this.containerState}
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000991 id="container-status"
Sean McCullough86b56862025-04-18 13:04:03 -0700992 ></sketch-container-status>
Autoformattercf570962025-04-30 17:27:39 +0000993
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000994 <!-- Last Commit section moved to sketch-container-status -->
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000995
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000996 <!-- Views section with tabs -->
997 <sketch-view-mode-select></sketch-view-mode-select>
Sean McCullough86b56862025-04-18 13:04:03 -0700998
999 <div class="refresh-control">
Sean McCulloughd3906e22025-04-29 17:32:14 +00001000 <button
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001001 id="stopButton"
1002 class="stop-button"
1003 ?disabled=${(this.containerState?.outstanding_llm_calls || 0) ===
1004 0 &&
1005 (this.containerState?.outstanding_tool_calls || []).length === 0}
1006 >
1007 <svg
1008 class="button-icon"
1009 xmlns="http://www.w3.org/2000/svg"
1010 viewBox="0 0 24 24"
1011 fill="none"
1012 stroke="currentColor"
1013 stroke-width="2"
1014 stroke-linecap="round"
1015 stroke-linejoin="round"
1016 >
1017 <rect x="6" y="6" width="12" height="12" />
1018 </svg>
1019 <span class="button-text">Stop</span>
Sean McCullough86b56862025-04-18 13:04:03 -07001020 </button>
Pokey Rule397871d2025-05-19 15:02:45 +01001021 <button
1022 id="endButton"
1023 class="end-button"
1024 @click=${this._handleEndClick}
1025 >
1026 <svg
1027 class="button-icon"
1028 xmlns="http://www.w3.org/2000/svg"
1029 viewBox="0 0 24 24"
1030 fill="none"
1031 stroke="currentColor"
1032 stroke-width="2"
1033 stroke-linecap="round"
1034 stroke-linejoin="round"
1035 >
1036 <path d="M18 6L6 18" />
1037 <path d="M6 6l12 12" />
1038 </svg>
1039 <span class="button-text">End</span>
1040 </button>
Sean McCullough86b56862025-04-18 13:04:03 -07001041
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +00001042 <div
1043 class="notifications-toggle"
1044 @click=${this._handleNotificationsToggle}
1045 title="${this.notificationsEnabled
1046 ? "Disable"
Philip Zeyligerbce3a132025-04-30 22:03:39 +00001047 : "Enable"} notifications when the agent completes its turn"
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +00001048 >
1049 <div
1050 class="bell-icon ${!this.notificationsEnabled
1051 ? "bell-disabled"
1052 : ""}"
1053 >
1054 <!-- Bell SVG icon -->
1055 <svg
1056 xmlns="http://www.w3.org/2000/svg"
1057 width="16"
1058 height="16"
1059 fill="currentColor"
1060 viewBox="0 0 16 16"
1061 >
1062 <path
1063 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"
1064 />
1065 </svg>
1066 </div>
Philip Zeyligerbc6b6292025-04-30 18:00:15 +00001067 </div>
1068
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001069 <sketch-call-status
Sean McCulloughd9d45812025-04-30 16:53:41 -07001070 .agentState=${this.containerState?.agent_state}
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001071 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
1072 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
Philip Zeyliger72318392025-05-14 02:56:07 +00001073 .isIdle=${this.messages.length > 0
1074 ? this.messages[this.messages.length - 1]?.end_of_turn &&
1075 !this.messages[this.messages.length - 1]?.parent_conversation_id
1076 : true}
Philip Zeyliger5e357022025-05-16 04:50:34 +00001077 .isDisconnected=${this.connectionStatus === "disconnected"}
Philip Zeyliger99a9a022025-04-27 15:15:25 +00001078 ></sketch-call-status>
Philip Zeyliger25f6ff12025-05-02 04:24:10 +00001079
1080 <sketch-network-status
1081 connection=${this.connectionStatus}
1082 error=${this.connectionErrorMessage}
1083 ></sketch-network-status>
Sean McCullough86b56862025-04-18 13:04:03 -07001084 </div>
1085 </div>
1086
Pokey Rule4097e532025-04-24 18:55:28 +01001087 <div id="view-container" ${ref(this.scrollContainerRef)}>
1088 <div id="view-container-inner">
1089 <div
1090 class="chat-view ${this.viewMode === "chat" ? "view-active" : ""}"
1091 >
1092 <sketch-timeline
1093 .messages=${this.messages}
1094 .scrollContainer=${this.scrollContainerRef}
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001095 .agentState=${this.containerState?.agent_state}
1096 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
1097 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
Pokey Rule4097e532025-04-24 18:55:28 +01001098 ></sketch-timeline>
1099 </div>
1100 <div
1101 class="diff-view ${this.viewMode === "diff" ? "view-active" : ""}"
1102 >
1103 <sketch-diff-view
1104 .commitHash=${this.currentCommitHash}
1105 ></sketch-diff-view>
1106 </div>
Autoformatter8c463622025-05-16 21:54:17 +00001107
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001108 <div
1109 class="diff2-view ${this.viewMode === "diff2" ? "view-active" : ""}"
1110 >
1111 <sketch-diff2-view
1112 .commit=${this.currentCommitHash}
1113 .gitService=${new DefaultGitDataService()}
1114 @diff-comment="${this._handleDiffComment}"
1115 ></sketch-diff2-view>
1116 </div>
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +00001117
Pokey Rule4097e532025-04-24 18:55:28 +01001118 <div
1119 class="terminal-view ${this.viewMode === "terminal"
1120 ? "view-active"
1121 : ""}"
1122 >
1123 <sketch-terminal></sketch-terminal>
1124 </div>
Sean McCullough86b56862025-04-18 13:04:03 -07001125 </div>
1126 </div>
1127
Pokey Rule4097e532025-04-24 18:55:28 +01001128 <div id="chat-input">
1129 <sketch-chat-input @send-chat="${this._sendChat}"></sketch-chat-input>
1130 </div>
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001131
Philip Zeyliger14fe75d2025-05-22 17:39:38 +00001132
Sean McCullough86b56862025-04-18 13:04:03 -07001133 `;
1134 }
1135
1136 /**
Sean McCullough86b56862025-04-18 13:04:03 -07001137 * Lifecycle callback when component is first connected to DOM
1138 */
1139 firstUpdated(): void {
1140 if (this.viewMode !== "chat") {
1141 return;
1142 }
1143
1144 // Initial scroll to bottom when component is first rendered
1145 setTimeout(
1146 () => this.scrollTo({ top: this.scrollHeight, behavior: "smooth" }),
Philip Zeyliger72682df2025-04-23 13:09:46 -07001147 50,
Sean McCullough86b56862025-04-18 13:04:03 -07001148 );
1149
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001150 // Setup stop button
1151 const stopButton = this.renderRoot?.querySelector(
1152 "#stopButton",
1153 ) as HTMLButtonElement;
1154 stopButton?.addEventListener("click", async () => {
1155 try {
Sean McCullough495cb962025-05-01 16:25:53 -07001156 const response = await fetch("cancel", {
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001157 method: "POST",
1158 headers: {
1159 "Content-Type": "application/json",
1160 },
1161 body: JSON.stringify({ reason: "User clicked stop button" }),
1162 });
1163 if (!response.ok) {
1164 console.error("Failed to cancel:", await response.text());
1165 }
1166 } catch (error) {
1167 console.error("Error cancelling operation:", error);
1168 }
1169 });
1170
Pokey Rule397871d2025-05-19 15:02:45 +01001171 // Setup end button
1172 const endButton = this.renderRoot?.querySelector(
1173 "#endButton",
1174 ) as HTMLButtonElement;
1175 // We're already using the @click binding in the HTML, so manual event listener not needed here
1176
Philip Zeyliger47b71c92025-04-30 15:43:39 +00001177 // Process any existing messages to find commit information
1178 if (this.messages && this.messages.length > 0) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001179 // Update last commit info via container status component
1180 if (this.containerStatusElement) {
1181 this.containerStatusElement.updateLastCommitInfo(this.messages);
1182 }
Philip Zeyliger47b71c92025-04-30 15:43:39 +00001183 }
Sean McCullough86b56862025-04-18 13:04:03 -07001184 }
1185}
1186
1187declare global {
1188 interface HTMLElementTagNameMap {
1189 "sketch-app-shell": SketchAppShell;
1190 }
1191}