blob: f37d9c97ee62e84d3f736cbfd2944129b8f1cdf7 [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";
Sean McCulloughbf66a2f2025-06-23 21:53:55 -07006import { SketchTailwindElement } from "./sketch-tailwind-element";
Philip Zeyliger2d4c48f2025-05-02 23:35:03 +00007
Pokey Rule4097e532025-04-24 18:55:28 +01008import "./sketch-chat-input";
9import "./sketch-container-status";
Philip Zeyliger00bcaef2025-05-30 04:21:15 +000010
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 Zeyliger00bcaef2025-05-30 04:21:15 +000025type ViewMode = "chat" | "diff2" | "terminal";
Sean McCullough86b56862025-04-18 13:04:03 -070026
27@customElement("sketch-app-shell")
Sean McCulloughbf66a2f2025-06-23 21:53:55 -070028export class SketchAppShell extends SketchTailwindElement {
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 McCulloughbf66a2f2025-06-23 21:53:55 -070043 // Note: CSS styles have been converted to Tailwind classes applied directly to HTML elements
44 // since this component now extends SketchTailwindElement which disables shadow DOM
Autoformattercf570962025-04-30 17:27:39 +000045
Sean McCulloughbf66a2f2025-06-23 21:53:55 -070046 // Override createRenderRoot to apply host styles for proper sizing while still using light DOM
47 createRenderRoot() {
48 // Use light DOM like SketchTailwindElement but still apply host styles
49 const style = document.createElement("style");
50 style.textContent = `
51 sketch-app-shell {
52 display: block;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070053 width: 100%;
Sean McCulloughbf66a2f2025-06-23 21:53:55 -070054 height: 100vh;
55 max-width: 100%;
56 box-sizing: border-box;
57 overflow: hidden;
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070058 }
Sean McCulloughbf66a2f2025-06-23 21:53:55 -070059 `;
60
61 // Add the style to the document head if not already present
62 if (!document.head.querySelector("style[data-sketch-app-shell]")) {
63 style.setAttribute("data-sketch-app-shell", "");
64 document.head.appendChild(style);
Philip Zeyliger272a90e2025-05-16 14:49:51 -070065 }
Autoformatter8c463622025-05-16 21:54:17 +000066
Sean McCulloughbf66a2f2025-06-23 21:53:55 -070067 return this;
68 }
Sean McCullough86b56862025-04-18 13:04:03 -070069
70 // Header bar: Network connection status details
71 @property()
72 connectionStatus: ConnectionStatus = "disconnected";
Autoformattercf570962025-04-30 17:27:39 +000073
Philip Zeyliger47b71c92025-04-30 15:43:39 +000074 // Track if the last commit info has been copied
75 @state()
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000076 // lastCommitCopied moved to sketch-container-status
Sean McCullough86b56862025-04-18 13:04:03 -070077
Philip Zeyligerbc6b6292025-04-30 18:00:15 +000078 // Track notification preferences
79 @state()
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +000080 notificationsEnabled: boolean = false;
81
82 // Track if the window is focused to control notifications
83 @state()
84 private _windowFocused: boolean = document.hasFocus();
Philip Zeyligerbc6b6292025-04-30 18:00:15 +000085
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070086 // Track if the todo panel should be visible
87 @state()
88 private _todoPanelVisible: boolean = false;
89
banksean65ff9092025-06-19 00:36:25 +000090 // Store scroll position for the chat view to preserve it when switching tabs
91 @state()
92 private _chatScrollPosition: number = 0;
93
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070094 // ResizeObserver for tracking chat input height changes
95 private chatInputResizeObserver: ResizeObserver | null = null;
96
Sean McCullough86b56862025-04-18 13:04:03 -070097 @property()
98 connectionErrorMessage: string = "";
99
Sean McCullough86b56862025-04-18 13:04:03 -0700100 // Chat messages
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100101 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700102 messages: AgentMessage[] = [];
Sean McCullough86b56862025-04-18 13:04:03 -0700103
104 @property()
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700105 set slug(value: string) {
106 const oldValue = this._slug;
107 this._slug = value;
108 this.requestUpdate("slug", oldValue);
109 // Update document title when slug property changes
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000110 this.updateDocumentTitle();
111 }
112
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700113 get slug(): string {
114 return this._slug;
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000115 }
116
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700117 private _slug: string = "";
Sean McCullough86b56862025-04-18 13:04:03 -0700118
119 private dataManager = new DataManager();
120
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100121 @property({ attribute: false })
Sean McCulloughd9f13372025-04-21 15:08:49 -0700122 containerState: State = {
Philip Zeyligerd03318d2025-05-08 13:09:12 -0700123 state_version: 2,
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700124 slug: "",
Sean McCulloughd9f13372025-04-21 15:08:49 -0700125 os: "",
126 message_count: 0,
127 hostname: "",
128 working_dir: "",
129 initial_commit: "",
Philip Zeyliger99a9a022025-04-27 15:15:25 +0000130 outstanding_llm_calls: 0,
131 outstanding_tool_calls: [],
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000132 session_id: "",
133 ssh_available: false,
Philip Zeyliger2c4db092025-04-28 16:57:50 -0700134 ssh_error: "",
135 in_container: false,
136 first_message_index: 0,
Philip Zeyliger64f60462025-06-16 13:57:10 -0700137 diff_lines_added: 0,
138 diff_lines_removed: 0,
Sean McCulloughd9f13372025-04-21 15:08:49 -0700139 };
Sean McCullough86b56862025-04-18 13:04:03 -0700140
Sean McCullough86b56862025-04-18 13:04:03 -0700141 // Mutation observer to detect when new messages are added
142 private mutationObserver: MutationObserver | null = null;
143
144 constructor() {
145 super();
146
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000147 // Reference to the container status element
148 this.containerStatusElement = null;
149
Sean McCullough86b56862025-04-18 13:04:03 -0700150 // Binding methods to this
151 this._handleViewModeSelect = this._handleViewModeSelect.bind(this);
Sean McCullough34bb09a2025-05-13 15:39:54 -0700152 this._handlePopState = this._handlePopState.bind(this);
Sean McCullough86b56862025-04-18 13:04:03 -0700153 this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
Sean McCullough485afc62025-04-28 14:28:39 -0700154 this._handleMutlipleChoiceSelected =
155 this._handleMutlipleChoiceSelected.bind(this);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000156 this._handleStopClick = this._handleStopClick.bind(this);
Pokey Rule397871d2025-05-19 15:02:45 +0100157 this._handleEndClick = this._handleEndClick.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000158 this._handleNotificationsToggle =
159 this._handleNotificationsToggle.bind(this);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000160 this._handleWindowFocus = this._handleWindowFocus.bind(this);
161 this._handleWindowBlur = this._handleWindowBlur.bind(this);
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000162
163 // Load notification preference from localStorage
164 try {
165 const savedPref = localStorage.getItem("sketch-notifications-enabled");
166 if (savedPref !== null) {
167 this.notificationsEnabled = savedPref === "true";
168 }
169 } catch (error) {
170 console.error("Error loading notification preference:", error);
171 }
Sean McCullough86b56862025-04-18 13:04:03 -0700172 }
173
174 // See https://lit.dev/docs/components/lifecycle/
175 connectedCallback() {
176 super.connectedCallback();
177
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000178 // Get reference to the container status element
179 setTimeout(() => {
180 this.containerStatusElement =
181 this.shadowRoot?.getElementById("container-status");
182 }, 0);
183
Sean McCullough86b56862025-04-18 13:04:03 -0700184 // Initialize client-side nav history.
185 const url = new URL(window.location.href);
186 const mode = url.searchParams.get("view") || "chat";
187 window.history.replaceState({ mode }, "", url.toString());
188
189 this.toggleViewMode(mode as ViewMode, false);
190 // Add popstate event listener to handle browser back/forward navigation
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100191 window.addEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700192
193 // Add event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100194 window.addEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100195 window.addEventListener("show-commit-diff", this._handleShowCommitDiff);
Sean McCullough86b56862025-04-18 13:04:03 -0700196
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000197 // Add window focus/blur listeners for controlling notifications
198 window.addEventListener("focus", this._handleWindowFocus);
199 window.addEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700200 window.addEventListener(
201 "multiple-choice-selected",
202 this._handleMutlipleChoiceSelected,
203 );
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000204
Sean McCullough86b56862025-04-18 13:04:03 -0700205 // register event listeners
206 this.dataManager.addEventListener(
207 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700208 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700209 );
210 this.dataManager.addEventListener(
211 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700212 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700213 );
214
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000215 // Set initial document title
216 this.updateDocumentTitle();
217
Sean McCullough86b56862025-04-18 13:04:03 -0700218 // Initialize the data manager
219 this.dataManager.initialize();
Autoformattercf570962025-04-30 17:27:39 +0000220
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000221 // Process existing messages for commit info
222 if (this.messages && this.messages.length > 0) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000223 // Update last commit info via container status component
224 setTimeout(() => {
225 if (this.containerStatusElement) {
226 this.containerStatusElement.updateLastCommitInfo(this.messages);
227 }
228 }, 100);
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000229 }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700230
231 // Check if todo panel should be visible on initial load
232 this.checkTodoPanelVisibility();
233
234 // Set up ResizeObserver for chat input to update todo panel height
235 this.setupChatInputObserver();
Sean McCullough86b56862025-04-18 13:04:03 -0700236 }
237
238 // See https://lit.dev/docs/components/lifecycle/
239 disconnectedCallback() {
240 super.disconnectedCallback();
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100241 window.removeEventListener("popstate", this._handlePopState);
Sean McCullough86b56862025-04-18 13:04:03 -0700242
243 // Remove event listeners
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100244 window.removeEventListener("view-mode-select", this._handleViewModeSelect);
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100245 window.removeEventListener("show-commit-diff", this._handleShowCommitDiff);
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000246 window.removeEventListener("focus", this._handleWindowFocus);
247 window.removeEventListener("blur", this._handleWindowBlur);
Sean McCullough485afc62025-04-28 14:28:39 -0700248 window.removeEventListener(
249 "multiple-choice-selected",
250 this._handleMutlipleChoiceSelected,
251 );
Sean McCullough86b56862025-04-18 13:04:03 -0700252
253 // unregister data manager event listeners
254 this.dataManager.removeEventListener(
255 "dataChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700256 this.handleDataChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700257 );
258 this.dataManager.removeEventListener(
259 "connectionStatusChanged",
Philip Zeyliger72682df2025-04-23 13:09:46 -0700260 this.handleConnectionStatusChanged.bind(this),
Sean McCullough86b56862025-04-18 13:04:03 -0700261 );
262
263 // Disconnect mutation observer if it exists
264 if (this.mutationObserver) {
Sean McCullough86b56862025-04-18 13:04:03 -0700265 this.mutationObserver.disconnect();
266 this.mutationObserver = null;
267 }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700268
269 // Disconnect chat input resize observer if it exists
270 if (this.chatInputResizeObserver) {
271 this.chatInputResizeObserver.disconnect();
272 this.chatInputResizeObserver = null;
273 }
Sean McCullough86b56862025-04-18 13:04:03 -0700274 }
275
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700276 updateUrlForViewMode(mode: ViewMode): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700277 // Get the current URL without search parameters
278 const url = new URL(window.location.href);
279
280 // Clear existing parameters
281 url.search = "";
282
283 // Only add view parameter if not in default chat view
284 if (mode !== "chat") {
285 url.searchParams.set("view", mode);
Philip Zeyliger00bcaef2025-05-30 04:21:15 +0000286 const diff2View = this.shadowRoot?.querySelector(
287 "sketch-diff2-view",
288 ) as SketchDiff2View;
Sean McCullough86b56862025-04-18 13:04:03 -0700289
Philip Zeyliger00bcaef2025-05-30 04:21:15 +0000290 // If in diff2 view and there's a commit hash, include that too
291 if (mode === "diff2" && diff2View?.commit) {
292 url.searchParams.set("commit", diff2View.commit);
Sean McCullough86b56862025-04-18 13:04:03 -0700293 }
294 }
295
296 // Update the browser history without reloading the page
297 window.history.pushState({ mode }, "", url.toString());
298 }
299
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100300 private _handlePopState(event: PopStateEvent) {
Sean McCullough86b56862025-04-18 13:04:03 -0700301 if (event.state && event.state.mode) {
302 this.toggleViewMode(event.state.mode, false);
303 } else {
304 this.toggleViewMode("chat", false);
305 }
306 }
307
308 /**
309 * Handle view mode selection event
310 */
311 private _handleViewModeSelect(event: CustomEvent) {
Philip Zeyliger00bcaef2025-05-30 04:21:15 +0000312 const mode = event.detail.mode as "chat" | "diff2" | "terminal";
Sean McCullough86b56862025-04-18 13:04:03 -0700313 this.toggleViewMode(mode, true);
314 }
315
316 /**
317 * Handle show commit diff event
318 */
319 private _handleShowCommitDiff(event: CustomEvent) {
320 const { commitHash } = event.detail;
321 if (commitHash) {
322 this.showCommitDiff(commitHash);
323 }
324 }
325
Sean McCullough485afc62025-04-28 14:28:39 -0700326 private _handleMultipleChoice(event: CustomEvent) {
327 window.console.log("_handleMultipleChoice", event);
328 this._sendChat;
329 }
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700330
331 private _handleDiffComment(event: CustomEvent) {
332 // Empty stub required by the event binding in the template
333 // Actual handling occurs at global level in sketch-chat-input component
334 }
Sean McCullough86b56862025-04-18 13:04:03 -0700335 /**
Sean McCullough86b56862025-04-18 13:04:03 -0700336 * Listen for commit diff event
337 * @param commitHash The commit hash to show diff for
338 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100339 private showCommitDiff(commitHash: string): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700340 // Store the commit hash
341 this.currentCommitHash = commitHash;
342
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700343 this.toggleViewMode("diff2", true);
Sean McCullough86b56862025-04-18 13:04:03 -0700344
Sean McCullough86b56862025-04-18 13:04:03 -0700345 this.updateComplete.then(() => {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700346 const diff2View = this.shadowRoot?.querySelector("sketch-diff2-view");
347 if (diff2View) {
348 (diff2View as SketchDiff2View).refreshDiffView();
Sean McCullough86b56862025-04-18 13:04:03 -0700349 }
350 });
351 }
352
353 /**
Philip Zeyliger00bcaef2025-05-30 04:21:15 +0000354 * Toggle between different view modes: chat, diff2, terminal
Sean McCullough86b56862025-04-18 13:04:03 -0700355 */
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100356 private toggleViewMode(mode: ViewMode, updateHistory: boolean): void {
Sean McCullough86b56862025-04-18 13:04:03 -0700357 // Don't do anything if the mode is already active
358 if (this.viewMode === mode) return;
359
banksean65ff9092025-06-19 00:36:25 +0000360 // Store scroll position if we're leaving the chat view
361 if (this.viewMode === "chat" && this.scrollContainerRef.value) {
362 // Only store scroll position if we actually have meaningful content
363 const scrollTop = this.scrollContainerRef.value.scrollTop;
364 const scrollHeight = this.scrollContainerRef.value.scrollHeight;
365 const clientHeight = this.scrollContainerRef.value.clientHeight;
366
367 // Store position only if we have scrollable content and have actually scrolled
368 if (scrollHeight > clientHeight && scrollTop > 0) {
369 this._chatScrollPosition = scrollTop;
370 }
371 }
372
Sean McCullough86b56862025-04-18 13:04:03 -0700373 // Update the view mode
374 this.viewMode = mode;
375
376 if (updateHistory) {
377 // Update URL with the current view mode
378 this.updateUrlForViewMode(mode);
379 }
380
381 // Wait for DOM update to complete
382 this.updateComplete.then(() => {
Sean McCulloughbf66a2f2025-06-23 21:53:55 -0700383 // Handle scroll position restoration for chat view
384 if (
385 mode === "chat" &&
386 this.scrollContainerRef.value &&
387 this._chatScrollPosition > 0
388 ) {
389 // Use requestAnimationFrame to ensure DOM is ready
390 requestAnimationFrame(() => {
391 if (this.scrollContainerRef.value) {
392 // Double-check that we're still in chat mode and the container is available
393 if (
394 this.viewMode === "chat" &&
395 this.scrollContainerRef.value.isConnected
396 ) {
397 this.scrollContainerRef.value.scrollTop =
398 this._chatScrollPosition;
399 }
400 }
401 });
Sean McCullough86b56862025-04-18 13:04:03 -0700402 }
403
Sean McCulloughbf66a2f2025-06-23 21:53:55 -0700404 // Handle diff2 view specific logic
405 if (mode === "diff2") {
406 // Refresh git/recentlog when Monaco diff view is opened
407 // This ensures branch information is always up-to-date, as branches can change frequently
408 const diff2ViewComp = this.querySelector("sketch-diff2-view");
409 if (diff2ViewComp) {
410 (diff2ViewComp as SketchDiff2View).refreshDiffView();
411 }
Sean McCullough86b56862025-04-18 13:04:03 -0700412 }
413
414 // Update view mode buttons
Sean McCulloughbf66a2f2025-06-23 21:53:55 -0700415 const viewModeSelect = this.querySelector("sketch-view-mode-select");
Sean McCullough86b56862025-04-18 13:04:03 -0700416 if (viewModeSelect) {
417 const event = new CustomEvent("update-active-mode", {
418 detail: { mode },
419 bubbles: true,
420 composed: true,
421 });
422 viewModeSelect.dispatchEvent(event);
423 }
Sean McCullough86b56862025-04-18 13:04:03 -0700424 });
425 }
426
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000427 /**
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700428 * Updates the document title based on current slug and connection status
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000429 */
430 private updateDocumentTitle(): void {
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700431 let docTitle = `sk: ${this.slug || "untitled"}`;
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000432
433 // Add red circle emoji if disconnected
434 if (this.connectionStatus === "disconnected") {
435 docTitle += " 🔴";
436 }
437
438 document.title = docTitle;
439 }
440
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000441 // Check and request notification permission if needed
442 private async checkNotificationPermission(): Promise<boolean> {
443 // Check if the Notification API is supported
444 if (!("Notification" in window)) {
445 console.log("This browser does not support notifications");
446 return false;
447 }
448
449 // Check if permission is already granted
450 if (Notification.permission === "granted") {
451 return true;
452 }
453
454 // If permission is not denied, request it
455 if (Notification.permission !== "denied") {
456 const permission = await Notification.requestPermission();
457 return permission === "granted";
458 }
459
460 return false;
461 }
462
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000463 // Handle notifications toggle click
464 private _handleNotificationsToggle(): void {
465 this.notificationsEnabled = !this.notificationsEnabled;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000466
467 // If enabling notifications, check permissions
468 if (this.notificationsEnabled) {
469 this.checkNotificationPermission();
470 }
471
472 // Save preference to localStorage
473 try {
474 localStorage.setItem(
475 "sketch-notifications-enabled",
476 String(this.notificationsEnabled),
477 );
478 } catch (error) {
479 console.error("Error saving notification preference:", error);
480 }
481 }
482
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000483 // Handle window focus event
484 private _handleWindowFocus(): void {
485 this._windowFocused = true;
486 }
487
488 // Handle window blur event
489 private _handleWindowBlur(): void {
490 this._windowFocused = false;
491 }
492
Philip Zeyliger5a85ffe2025-06-21 21:27:44 -0700493 // Get the last user or agent message (ignore system messages like commit, error, etc.)
494 // For example, when Sketch notices a new commit, it'll send a message,
495 // but it's still idle!
496 private getLastUserOrAgentMessage(): AgentMessage | null {
497 for (let i = this.messages.length - 1; i >= 0; i--) {
498 const message = this.messages[i];
499 if (message.type === "user" || message.type === "agent") {
500 return message;
501 }
502 }
503 return null;
504 }
505
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000506 // Show notification for message with EndOfTurn=true
507 private async showEndOfTurnNotification(
508 message: AgentMessage,
509 ): Promise<void> {
510 // Don't show notifications if they're disabled
511 if (!this.notificationsEnabled) return;
512
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000513 // Don't show notifications if the window is focused
514 if (this._windowFocused) return;
515
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000516 // Check if we have permission to show notifications
517 const hasPermission = await this.checkNotificationPermission();
518 if (!hasPermission) return;
519
Philip Zeyliger32011332025-04-30 20:59:40 +0000520 // Only show notifications for agent messages with end_of_turn=true and no parent_conversation_id
521 if (
522 message.type !== "agent" ||
523 !message.end_of_turn ||
524 message.parent_conversation_id
525 )
526 return;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000527
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700528 // Create a title that includes the sketch slug
529 const notificationTitle = `Sketch: ${this.slug || "untitled"}`;
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000530
531 // Extract the beginning of the message content (first 100 chars)
532 const messagePreview = message.content
533 ? message.content.substring(0, 100) +
534 (message.content.length > 100 ? "..." : "")
535 : "Agent has completed its turn";
536
537 // Create and show the notification
538 try {
539 new Notification(notificationTitle, {
540 body: messagePreview,
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000541 icon: "https://sketch.dev/favicon.ico", // Use sketch.dev favicon for notification
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000542 });
543 } catch (error) {
544 console.error("Error showing notification:", error);
545 }
546 }
547
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700548 // Check if todo panel should be visible based on latest todo content from messages or state
549 private checkTodoPanelVisibility(): void {
550 // Find the latest todo content from messages first
551 let latestTodoContent = "";
552 for (let i = this.messages.length - 1; i >= 0; i--) {
553 const message = this.messages[i];
554 if (message.todo_content !== undefined) {
555 latestTodoContent = message.todo_content || "";
556 break;
557 }
558 }
559
560 // If no todo content found in messages, check the current state
561 if (latestTodoContent === "" && this.containerState?.todo_content) {
562 latestTodoContent = this.containerState.todo_content;
563 }
564
565 // Parse the todo data to check if there are any actual todos
566 let hasTodos = false;
567 if (latestTodoContent.trim()) {
568 try {
569 const todoData = JSON.parse(latestTodoContent);
570 hasTodos = todoData.items && todoData.items.length > 0;
571 } catch (error) {
572 // Invalid JSON, treat as no todos
573 hasTodos = false;
574 }
575 }
576
577 this._todoPanelVisible = hasTodos;
578
579 // Update todo panel content if visible
580 if (hasTodos) {
581 const todoPanel = this.shadowRoot?.querySelector(
582 "sketch-todo-panel",
583 ) as any;
584 if (todoPanel && todoPanel.updateTodoContent) {
585 todoPanel.updateTodoContent(latestTodoContent);
586 }
587 }
588 }
589
Sean McCullough86b56862025-04-18 13:04:03 -0700590 private handleDataChanged(eventData: {
591 state: State;
Sean McCulloughd9f13372025-04-21 15:08:49 -0700592 newMessages: AgentMessage[];
Sean McCullough86b56862025-04-18 13:04:03 -0700593 }): void {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000594 const { state, newMessages } = eventData;
Sean McCullough86b56862025-04-18 13:04:03 -0700595
596 // Update state if we received it
597 if (state) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000598 // Ensure we're using the latest call status to prevent indicators from being stuck
Autoformatterf830c9d2025-04-30 18:16:01 +0000599 if (
600 state.outstanding_llm_calls === 0 &&
601 state.outstanding_tool_calls.length === 0
602 ) {
Josh Bleecher Snydere81233f2025-04-30 04:05:41 +0000603 // Force reset containerState calls when nothing is reported as in progress
604 state.outstanding_llm_calls = 0;
605 state.outstanding_tool_calls = [];
606 }
Autoformatterf830c9d2025-04-30 18:16:01 +0000607
Sean McCullough86b56862025-04-18 13:04:03 -0700608 this.containerState = state;
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700609 this.slug = state.slug || "";
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000610
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -0700611 // Update document title when sketch slug changes
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000612 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700613 }
614
Sean McCullough86b56862025-04-18 13:04:03 -0700615 // Update messages
banksean65ff9092025-06-19 00:36:25 +0000616 const oldMessageCount = this.messages.length;
Pokey Rulee2a8c2f2025-04-23 15:09:25 +0100617 this.messages = aggregateAgentMessages(this.messages, newMessages);
Autoformattercf570962025-04-30 17:27:39 +0000618
banksean65ff9092025-06-19 00:36:25 +0000619 // If new messages were added and we're in chat view, reset stored scroll position
620 // so the timeline can auto-scroll to bottom for new content
621 if (this.messages.length > oldMessageCount && this.viewMode === "chat") {
622 // Only reset if we were near the bottom (indicating user wants to follow new messages)
623 if (this.scrollContainerRef.value) {
624 const scrollTop = this.scrollContainerRef.value.scrollTop;
625 const scrollHeight = this.scrollContainerRef.value.scrollHeight;
626 const clientHeight = this.scrollContainerRef.value.clientHeight;
627 const isNearBottom = scrollTop + clientHeight >= scrollHeight - 50; // 50px tolerance
628
629 if (isNearBottom) {
630 this._chatScrollPosition = 0; // Reset stored position to allow auto-scroll
631 }
632 }
633 }
634
Philip Zeyliger47b71c92025-04-30 15:43:39 +0000635 // Process new messages to find commit messages
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000636 // Update last commit info via container status component
637 if (this.containerStatusElement) {
638 this.containerStatusElement.updateLastCommitInfo(newMessages);
639 }
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000640
641 // Check for agent messages with end_of_turn=true and show notifications
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000642 if (newMessages && newMessages.length > 0) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000643 for (const message of newMessages) {
Philip Zeyliger32011332025-04-30 20:59:40 +0000644 if (
645 message.type === "agent" &&
646 message.end_of_turn &&
647 !message.parent_conversation_id
648 ) {
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000649 this.showEndOfTurnNotification(message);
650 break; // Only show one notification per batch of messages
651 }
652 }
653 }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700654
655 // Check if todo panel should be visible after agent loop iteration
656 this.checkTodoPanelVisibility();
657
658 // Ensure chat input observer is set up when new data comes in
659 if (!this.chatInputResizeObserver) {
660 this.setupChatInputObserver();
661 }
Sean McCullough86b56862025-04-18 13:04:03 -0700662 }
663
664 private handleConnectionStatusChanged(
665 status: ConnectionStatus,
Philip Zeyliger72682df2025-04-23 13:09:46 -0700666 errorMessage?: string,
Sean McCullough86b56862025-04-18 13:04:03 -0700667 ): void {
668 this.connectionStatus = status;
669 this.connectionErrorMessage = errorMessage || "";
Philip Zeyliger9b999b02025-04-25 16:31:50 +0000670
671 // Update document title when connection status changes
672 this.updateDocumentTitle();
Sean McCullough86b56862025-04-18 13:04:03 -0700673 }
674
Sean McCulloughd3906e22025-04-29 17:32:14 +0000675 private async _handleStopClick(): Promise<void> {
676 try {
677 const response = await fetch("cancel", {
678 method: "POST",
679 headers: {
680 "Content-Type": "application/json",
681 },
682 body: JSON.stringify({ reason: "user requested cancellation" }),
683 });
684
685 if (!response.ok) {
686 const errorData = await response.text();
687 throw new Error(
688 `Failed to stop operation: ${response.status} - ${errorData}`,
689 );
690 }
691
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000692 // Stop request sent
Sean McCulloughd3906e22025-04-29 17:32:14 +0000693 } catch (error) {
694 console.error("Error stopping operation:", error);
Sean McCulloughd3906e22025-04-29 17:32:14 +0000695 }
696 }
697
Pokey Rule397871d2025-05-19 15:02:45 +0100698 private async _handleEndClick(event?: Event): Promise<void> {
699 if (event) {
700 event.preventDefault();
701 event.stopPropagation();
702 }
Philip Zeyligerb5739402025-06-02 07:04:34 -0700703
Philip Zeyliger16098932025-06-04 11:02:55 -0700704 // Show confirmation dialog
705 const confirmed = window.confirm(
706 "Ending the session will shut down the underlying container. Are you sure?",
707 );
708 if (!confirmed) return;
Pokey Rule397871d2025-05-19 15:02:45 +0100709
710 try {
711 const response = await fetch("end", {
712 method: "POST",
713 headers: {
714 "Content-Type": "application/json",
715 },
Philip Zeyliger16098932025-06-04 11:02:55 -0700716 body: JSON.stringify({ reason: "user requested end of session" }),
Pokey Rule397871d2025-05-19 15:02:45 +0100717 });
718
719 if (!response.ok) {
720 const errorData = await response.text();
721 throw new Error(
722 `Failed to end session: ${response.status} - ${errorData}`,
723 );
724 }
725
726 // After successful response, redirect to messages view
727 // Extract the session ID from the URL
728 const currentUrl = window.location.href;
729 // The URL pattern should be like https://sketch.dev/s/cs71-8qa6-1124-aw79/
730 const urlParts = currentUrl.split("/");
731 let sessionId = "";
732
733 // Find the session ID in the URL (should be after /s/)
734 for (let i = 0; i < urlParts.length; i++) {
735 if (urlParts[i] === "s" && i + 1 < urlParts.length) {
736 sessionId = urlParts[i + 1];
737 break;
738 }
739 }
740
741 if (sessionId) {
742 // Create the messages URL
743 const messagesUrl = `/messages/${sessionId}`;
744 // Redirect to messages view
745 window.location.href = messagesUrl;
746 }
747
748 // End request sent - connection will be closed by server
749 } catch (error) {
750 console.error("Error ending session:", error);
751 }
752 }
753
Sean McCullough485afc62025-04-28 14:28:39 -0700754 async _handleMutlipleChoiceSelected(e: CustomEvent) {
755 const chatInput = this.shadowRoot?.querySelector(
756 "sketch-chat-input",
757 ) as SketchChatInput;
758 if (chatInput) {
Josh Bleecher Snyder6cad8612025-05-30 19:25:39 +0000759 if (chatInput.content && chatInput.content.trim() !== "") {
760 chatInput.content += "\n\n";
761 }
762 chatInput.content += e.detail.responseText;
Sean McCullough485afc62025-04-28 14:28:39 -0700763 chatInput.focus();
Josh Bleecher Snyder6cad8612025-05-30 19:25:39 +0000764 // Adjust textarea height to accommodate new content
765 requestAnimationFrame(() => {
766 if (chatInput.adjustChatSpacing) {
767 chatInput.adjustChatSpacing();
768 }
769 });
Sean McCullough485afc62025-04-28 14:28:39 -0700770 }
771 }
772
Sean McCullough86b56862025-04-18 13:04:03 -0700773 async _sendChat(e: CustomEvent) {
774 console.log("app shell: _sendChat", e);
Sean McCullough485afc62025-04-28 14:28:39 -0700775 e.preventDefault();
776 e.stopPropagation();
Sean McCullough86b56862025-04-18 13:04:03 -0700777 const message = e.detail.message?.trim();
778 if (message == "") {
779 return;
780 }
781 try {
Josh Bleecher Snyder98b64d12025-05-12 19:42:43 +0000782 // Always switch to chat view when sending a message so user can see processing
783 if (this.viewMode !== "chat") {
784 this.toggleViewMode("chat", true);
785 }
Autoformatter5c7f9572025-05-13 01:17:31 +0000786
Sean McCullough86b56862025-04-18 13:04:03 -0700787 // Send the message to the server
788 const response = await fetch("chat", {
789 method: "POST",
790 headers: {
791 "Content-Type": "application/json",
792 },
793 body: JSON.stringify({ message }),
794 });
795
796 if (!response.ok) {
797 const errorData = await response.text();
798 throw new Error(`Server error: ${response.status} - ${errorData}`);
799 }
Sean McCullough86b56862025-04-18 13:04:03 -0700800 } catch (error) {
801 console.error("Error sending chat message:", error);
802 const statusText = document.getElementById("statusText");
803 if (statusText) {
804 statusText.textContent = "Error sending message";
805 }
806 }
807 }
808
Pokey Rule4097e532025-04-24 18:55:28 +0100809 private scrollContainerRef = createRef<HTMLElement>();
810
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700811 /**
812 * Set up ResizeObserver to monitor chat input height changes
813 */
814 private setupChatInputObserver(): void {
815 // Wait for DOM to be ready
816 this.updateComplete.then(() => {
817 const chatInputElement = this.shadowRoot?.querySelector("#chat-input");
818 if (chatInputElement && !this.chatInputResizeObserver) {
819 this.chatInputResizeObserver = new ResizeObserver((entries) => {
820 for (const entry of entries) {
821 this.updateTodoPanelHeight(entry.contentRect.height);
822 }
823 });
824
825 this.chatInputResizeObserver.observe(chatInputElement);
826
827 // Initial height calculation
828 const rect = chatInputElement.getBoundingClientRect();
829 this.updateTodoPanelHeight(rect.height);
830 }
831 });
832 }
833
834 /**
835 * Update the CSS custom property that controls todo panel bottom position
836 */
837 private updateTodoPanelHeight(chatInputHeight: number): void {
838 // Add some padding (20px) between todo panel and chat input
839 const bottomOffset = chatInputHeight;
840
841 // Update the CSS custom property on the host element
842 this.style.setProperty("--chat-input-height", `${bottomOffset}px`);
843 }
844
Sean McCullough86b56862025-04-18 13:04:03 -0700845 render() {
846 return html`
Sean McCulloughbf66a2f2025-06-23 21:53:55 -0700847 <!-- Main container: flex column, full height, system font, hidden overflow-x -->
848 <div
849 class="block font-sans text-gray-800 leading-relaxed h-screen w-full relative overflow-x-hidden flex flex-col"
850 >
851 <!-- Top banner: flex row, space between, border bottom, shadow -->
852 <div
853 id="top-banner"
854 class="flex self-stretch justify-between items-center px-5 pr-8 mb-0 border-b border-gray-200 gap-5 bg-white shadow-md w-full h-12"
855 >
856 <!-- Title container -->
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000857 <div
Sean McCulloughbf66a2f2025-06-23 21:53:55 -0700858 class="flex flex-col whitespace-nowrap overflow-hidden text-ellipsis max-w-[30%] md:max-w-1/2 sm:max-w-[60%] py-1.5"
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000859 >
Sean McCulloughbf66a2f2025-06-23 21:53:55 -0700860 <h1
861 class="text-lg md:text-base sm:text-sm font-semibold m-0 min-w-24 whitespace-nowrap overflow-hidden text-ellipsis"
Philip Zeyligerdb5e9b42025-04-30 19:58:13 +0000862 >
Sean McCulloughbf66a2f2025-06-23 21:53:55 -0700863 ${this.containerState?.skaband_addr
864 ? html`<a
865 href="${this.containerState.skaband_addr}"
866 target="_blank"
867 rel="noopener noreferrer"
868 class="text-inherit no-underline transition-opacity duration-200 ease-in-out flex items-center gap-2 hover:opacity-80 hover:underline"
869 >
870 <img
871 src="${this.containerState.skaband_addr}/sketch.dev.png"
872 alt="sketch"
873 class="w-5 h-5 md:w-[18px] md:h-[18px] sm:w-4 sm:h-4 rounded-sm"
874 />
875 sketch
876 </a>`
877 : html`sketch`}
878 </h1>
879 <h2
880 class="m-0 p-0 text-gray-600 text-sm font-normal italic whitespace-nowrap overflow-hidden text-ellipsis"
881 >
882 ${this.slug}
883 </h2>
Philip Zeyligerbc6b6292025-04-30 18:00:15 +0000884 </div>
885
Sean McCulloughbf66a2f2025-06-23 21:53:55 -0700886 <!-- Container status info moved above tabs -->
887 <sketch-container-status
888 .state=${this.containerState}
889 id="container-status"
890 ></sketch-container-status>
Philip Zeyliger25f6ff12025-05-02 04:24:10 +0000891
Sean McCulloughbf66a2f2025-06-23 21:53:55 -0700892 <!-- Last Commit section moved to sketch-container-status -->
893
894 <!-- Views section with tabs -->
895 <sketch-view-mode-select
896 .diffLinesAdded=${this.containerState?.diff_lines_added || 0}
897 .diffLinesRemoved=${this.containerState?.diff_lines_removed || 0}
898 ></sketch-view-mode-select>
899
900 <!-- Control buttons and status -->
901 <div
902 class="flex items-center mb-0 flex-nowrap whitespace-nowrap flex-shrink-0 gap-4 pl-4 mr-12"
903 >
904 <button
905 id="stopButton"
906 class="bg-red-600 hover:bg-red-700 disabled:bg-red-300 disabled:cursor-not-allowed disabled:opacity-70 text-white border-none px-2.5 py-1 xl:px-1.5 rounded cursor-pointer text-xs mr-1.5 flex items-center gap-1.5 transition-colors"
907 ?disabled=${(this.containerState?.outstanding_llm_calls || 0) ===
908 0 &&
909 (this.containerState?.outstanding_tool_calls || []).length === 0}
910 >
911 <svg
912 class="w-4 h-4"
913 xmlns="http://www.w3.org/2000/svg"
914 viewBox="0 0 24 24"
915 fill="none"
916 stroke="currentColor"
917 stroke-width="2"
918 stroke-linecap="round"
919 stroke-linejoin="round"
920 >
921 <rect x="6" y="6" width="12" height="12" />
922 </svg>
923 <span class="xl:hidden">Stop</span>
924 </button>
925 <button
926 id="endButton"
927 class="bg-gray-600 hover:bg-gray-700 disabled:bg-gray-400 disabled:cursor-not-allowed disabled:opacity-70 text-white border-none px-2.5 py-1 xl:px-1.5 rounded cursor-pointer text-xs mr-1.5 flex items-center gap-1.5 transition-colors"
928 @click=${this._handleEndClick}
929 >
930 <svg
931 class="w-4 h-4"
932 xmlns="http://www.w3.org/2000/svg"
933 viewBox="0 0 24 24"
934 fill="none"
935 stroke="currentColor"
936 stroke-width="2"
937 stroke-linecap="round"
938 stroke-linejoin="round"
939 >
940 <path d="M18 6L6 18" />
941 <path d="M6 6l12 12" />
942 </svg>
943 <span class="xl:hidden">End</span>
944 </button>
945
946 <div
947 class="flex items-center text-xs mr-2.5 cursor-pointer"
948 @click=${this._handleNotificationsToggle}
949 title="${this.notificationsEnabled
950 ? "Disable"
951 : "Enable"} notifications when the agent completes its turn"
952 >
953 <div
954 class="w-5 h-5 relative inline-flex items-center justify-center"
955 >
956 <!-- Bell SVG icon -->
957 <svg
958 xmlns="http://www.w3.org/2000/svg"
959 width="16"
960 height="16"
961 fill="currentColor"
962 viewBox="0 0 16 16"
963 class="${!this.notificationsEnabled ? "relative z-10" : ""}"
964 >
965 <path
966 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"
967 />
968 </svg>
969 ${!this.notificationsEnabled
970 ? html`<div
971 class="absolute w-0.5 h-6 bg-red-600 rotate-45 origin-center"
972 ></div>`
973 : ""}
974 </div>
975 </div>
976
977 <sketch-call-status
978 .agentState=${this.containerState?.agent_state}
979 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
980 .toolCalls=${this.containerState?.outstanding_tool_calls || []}
981 .isIdle=${(() => {
982 const lastUserOrAgentMessage = this.getLastUserOrAgentMessage();
983 return lastUserOrAgentMessage
984 ? lastUserOrAgentMessage.end_of_turn &&
985 !lastUserOrAgentMessage.parent_conversation_id
986 : true;
987 })()}
988 .isDisconnected=${this.connectionStatus === "disconnected"}
989 ></sketch-call-status>
990
991 <sketch-network-status
992 connection=${this.connectionStatus}
993 error=${this.connectionErrorMessage}
994 ></sketch-network-status>
995 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700996 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700997
Sean McCulloughbf66a2f2025-06-23 21:53:55 -0700998 <!-- Main content area: scrollable, flex-1 -->
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700999 <div
Sean McCulloughbf66a2f2025-06-23 21:53:55 -07001000 id="view-container"
1001 ${ref(this.scrollContainerRef)}
1002 class="self-stretch overflow-y-auto flex-1 flex flex-col min-h-0"
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001003 >
Pokey Rule4097e532025-04-24 18:55:28 +01001004 <div
Sean McCulloughbf66a2f2025-06-23 21:53:55 -07001005 id="view-container-inner"
1006 class="${this.viewMode === "diff2"
1007 ? "max-w-full w-full h-full p-0 flex flex-col flex-1 min-h-0"
1008 : this._todoPanelVisible && this.viewMode === "chat"
1009 ? "max-w-none w-full m-0 px-5"
1010 : "max-w-6xl w-[calc(100%-40px)] mx-auto"} relative pb-2.5 pt-2.5 flex flex-col h-full"
Pokey Rule4097e532025-04-24 18:55:28 +01001011 >
Sean McCulloughbf66a2f2025-06-23 21:53:55 -07001012 <!-- Chat View -->
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001013 <div
Sean McCulloughbf66a2f2025-06-23 21:53:55 -07001014 class="chat-view ${this.viewMode === "chat"
1015 ? "view-active flex flex-col"
1016 : "hidden"} w-full h-full"
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001017 >
Sean McCulloughbf66a2f2025-06-23 21:53:55 -07001018 <div
1019 class="${this._todoPanelVisible && this.viewMode === "chat"
1020 ? "mr-[400px] xl:mr-[350px] lg:mr-[300px] md:mr-0 w-[calc(100%-400px)] xl:w-[calc(100%-350px)] lg:w-[calc(100%-300px)] md:w-full"
1021 : "mr-0"} flex-1 flex flex-col w-full h-full transition-[margin-right] duration-200 ease-in-out"
1022 >
1023 <sketch-timeline
1024 .messages=${this.messages}
1025 .scrollContainer=${this.scrollContainerRef}
1026 .agentState=${this.containerState?.agent_state}
1027 .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
1028 .toolCalls=${this.containerState?.outstanding_tool_calls ||
1029 []}
1030 .firstMessageIndex=${this.containerState
1031 ?.first_message_index || 0}
1032 .state=${this.containerState}
1033 .dataManager=${this.dataManager}
1034 ></sketch-timeline>
1035 </div>
1036 </div>
1037
1038 <!-- Todo panel positioned outside the main flow - only visible in chat view -->
1039 <div
1040 class="${this._todoPanelVisible && this.viewMode === "chat"
1041 ? "block"
1042 : "hidden"} fixed top-12 right-4 w-[400px] xl:w-[350px] lg:w-[300px] md:hidden z-[100] transition-[bottom] duration-200 ease-in-out"
1043 style="bottom: var(--chat-input-height, 90px); background: linear-gradient(to bottom, #fafafa 0%, #fafafa 90%, rgba(250, 250, 250, 0.5) 95%, rgba(250, 250, 250, 0.2) 100%); border-left: 1px solid #e0e0e0;"
1044 >
1045 <sketch-todo-panel
1046 .visible=${this._todoPanelVisible && this.viewMode === "chat"}
1047 ></sketch-todo-panel>
1048 </div>
1049 <!-- Diff2 View -->
1050 <div
1051 class="diff2-view ${this.viewMode === "diff2"
1052 ? "view-active flex-1 overflow-hidden min-h-0 flex flex-col h-full"
1053 : "hidden"} w-full h-full"
1054 >
1055 <sketch-diff2-view
1056 .commit=${this.currentCommitHash}
1057 .gitService=${new DefaultGitDataService()}
1058 @diff-comment="${this._handleDiffComment}"
1059 ></sketch-diff2-view>
1060 </div>
1061
1062 <!-- Terminal View -->
1063 <div
1064 class="terminal-view ${this.viewMode === "terminal"
1065 ? "view-active flex flex-col"
1066 : "hidden"} w-full h-full"
1067 >
1068 <sketch-terminal></sketch-terminal>
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001069 </div>
1070 </div>
Sean McCullough86b56862025-04-18 13:04:03 -07001071 </div>
Sean McCullough86b56862025-04-18 13:04:03 -07001072
Sean McCulloughbf66a2f2025-06-23 21:53:55 -07001073 <!-- Chat input fixed at bottom -->
1074 <div
1075 id="chat-input"
1076 class="self-end w-full shadow-[0_-2px_10px_rgba(0,0,0,0.1)]"
1077 >
1078 <sketch-chat-input @send-chat="${this._sendChat}"></sketch-chat-input>
1079 </div>
Pokey Rule4097e532025-04-24 18:55:28 +01001080 </div>
Sean McCullough86b56862025-04-18 13:04:03 -07001081 `;
1082 }
1083
1084 /**
Sean McCullough86b56862025-04-18 13:04:03 -07001085 * Lifecycle callback when component is first connected to DOM
1086 */
1087 firstUpdated(): void {
1088 if (this.viewMode !== "chat") {
1089 return;
1090 }
1091
1092 // Initial scroll to bottom when component is first rendered
1093 setTimeout(
1094 () => this.scrollTo({ top: this.scrollHeight, behavior: "smooth" }),
Philip Zeyliger72682df2025-04-23 13:09:46 -07001095 50,
Sean McCullough86b56862025-04-18 13:04:03 -07001096 );
1097
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001098 // Setup stop button
1099 const stopButton = this.renderRoot?.querySelector(
1100 "#stopButton",
1101 ) as HTMLButtonElement;
1102 stopButton?.addEventListener("click", async () => {
1103 try {
Sean McCullough495cb962025-05-01 16:25:53 -07001104 const response = await fetch("cancel", {
Philip Zeyliger2c4db092025-04-28 16:57:50 -07001105 method: "POST",
1106 headers: {
1107 "Content-Type": "application/json",
1108 },
1109 body: JSON.stringify({ reason: "User clicked stop button" }),
1110 });
1111 if (!response.ok) {
1112 console.error("Failed to cancel:", await response.text());
1113 }
1114 } catch (error) {
1115 console.error("Error cancelling operation:", error);
1116 }
1117 });
1118
Pokey Rule397871d2025-05-19 15:02:45 +01001119 // Setup end button
1120 const endButton = this.renderRoot?.querySelector(
1121 "#endButton",
1122 ) as HTMLButtonElement;
1123 // We're already using the @click binding in the HTML, so manual event listener not needed here
1124
Philip Zeyliger47b71c92025-04-30 15:43:39 +00001125 // Process any existing messages to find commit information
1126 if (this.messages && this.messages.length > 0) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001127 // Update last commit info via container status component
1128 if (this.containerStatusElement) {
1129 this.containerStatusElement.updateLastCommitInfo(this.messages);
1130 }
Philip Zeyliger47b71c92025-04-30 15:43:39 +00001131 }
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001132
1133 // Set up chat input height observer for todo panel
1134 this.setupChatInputObserver();
Sean McCullough86b56862025-04-18 13:04:03 -07001135 }
1136}
1137
1138declare global {
1139 interface HTMLElementTagNameMap {
1140 "sketch-app-shell": SketchAppShell;
1141 }
1142}