blob: 2e83df0c9333f7a68ad8afa4e2d413edadcbcec7 [file] [log] [blame]
Philip Zeyliger6dc90c02025-07-03 20:12:49 -07001import { State, AgentMessage, Usage, Port } from "../types";
Sean McCullough7e36a042025-06-25 08:45:18 +00002import { html } from "lit";
Philip Zeyligere66db3e2025-04-27 15:40:39 +00003import { customElement, property, state } from "lit/decorators.js";
Josh Bleecher Snydera0801ad2025-04-25 19:34:53 +00004import { formatNumber } from "../utils";
Sean McCullough7e36a042025-06-25 08:45:18 +00005import { SketchTailwindElement } from "./sketch-tailwind-element";
Philip Zeyliger254c49f2025-07-17 17:26:24 -07006import "./sketch-push-button";
Sean McCullough86b56862025-04-18 13:04:03 -07007
8@customElement("sketch-container-status")
Sean McCullough7e36a042025-06-25 08:45:18 +00009export class SketchContainerStatus extends SketchTailwindElement {
Sean McCullough86b56862025-04-18 13:04:03 -070010 // Header bar: Container status details
11
12 @property()
13 state: State;
14
Philip Zeyligere66db3e2025-04-27 15:40:39 +000015 @state()
16 showDetails: boolean = false;
17
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000018 @state()
19 lastCommit: { hash: string; pushedBranch?: string } | null = null;
20
21 @state()
22 lastCommitCopied: boolean = false;
23
Sean McCulloughc37e0662025-07-03 08:46:21 -070024 @state()
25 latestUsage: Usage | null = null;
26
Philip Zeyliger6dc90c02025-07-03 20:12:49 -070027 @state()
28 showPortsPopup: boolean = false;
29
30 @state()
31 previousPorts: Port[] = [];
32
33 @state()
34 highlightedPorts: Set<number> = new Set();
35
Sean McCullough7e36a042025-06-25 08:45:18 +000036 // CSS animations that can't be easily replaced with Tailwind
37 connectedCallback() {
38 super.connectedCallback();
39 // Add custom CSS animations to the document head if not already present
40 if (!document.querySelector("#container-status-animations")) {
41 const style = document.createElement("style");
42 style.id = "container-status-animations";
43 style.textContent = `
44 @keyframes pulse-custom {
45 0% { transform: scale(1); opacity: 1; }
46 50% { transform: scale(1.05); opacity: 0.8; }
47 100% { transform: scale(1); opacity: 1; }
48 }
49 .pulse-custom {
50 animation: pulse-custom 1.5s ease-in-out;
51 background-color: rgba(38, 132, 255, 0.1);
52 border-radius: 3px;
53 }
54 `;
55 document.head.appendChild(style);
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000056 }
Sean McCullough7e36a042025-06-25 08:45:18 +000057 }
Sean McCullough86b56862025-04-18 13:04:03 -070058
59 constructor() {
60 super();
Philip Zeyligere66db3e2025-04-27 15:40:39 +000061 this._toggleInfoDetails = this._toggleInfoDetails.bind(this);
62
63 // Close the info panel when clicking outside of it
64 document.addEventListener("click", (event) => {
65 if (this.showDetails && !this.contains(event.target as Node)) {
66 this.showDetails = false;
67 this.requestUpdate();
68 }
Philip Zeyliger6dc90c02025-07-03 20:12:49 -070069 // Close the ports popup when clicking outside of it
70 if (this.showPortsPopup && !this.contains(event.target as Node)) {
71 this.showPortsPopup = false;
72 this.requestUpdate();
73 }
Philip Zeyligere66db3e2025-04-27 15:40:39 +000074 });
75 }
76
77 /**
78 * Toggle the display of detailed information
79 */
80 private _toggleInfoDetails(event: Event) {
81 event.stopPropagation();
82 this.showDetails = !this.showDetails;
83 this.requestUpdate();
Sean McCullough86b56862025-04-18 13:04:03 -070084 }
85
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000086 /**
87 * Update the last commit information based on messages
88 */
89 public updateLastCommitInfo(newMessages: AgentMessage[]): void {
90 if (!newMessages || newMessages.length === 0) return;
91
92 // Process messages in chronological order (latest last)
93 for (const message of newMessages) {
94 if (
95 message.type === "commit" &&
96 message.commits &&
97 message.commits.length > 0
98 ) {
99 // Get the first commit from the list
100 const commit = message.commits[0];
101 if (commit) {
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700102 // Check if the commit hash has changed
103 const hasChanged =
104 !this.lastCommit || this.lastCommit.hash !== commit.hash;
105
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000106 this.lastCommit = {
107 hash: commit.hash,
108 pushedBranch: commit.pushed_branch,
109 };
110 this.lastCommitCopied = false;
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700111
112 // Add pulse animation if the commit changed
113 if (hasChanged) {
114 // Find the last commit element
115 setTimeout(() => {
Sean McCullough7e36a042025-06-25 08:45:18 +0000116 const lastCommitEl = this.querySelector(".last-commit-main");
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700117 if (lastCommitEl) {
118 // Add the pulse class
Sean McCullough7e36a042025-06-25 08:45:18 +0000119 lastCommitEl.classList.add("pulse-custom");
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700120
121 // Remove the pulse class after animation completes
122 setTimeout(() => {
Sean McCullough7e36a042025-06-25 08:45:18 +0000123 lastCommitEl.classList.remove("pulse-custom");
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700124 }, 1500);
125 }
126 }, 0);
127 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000128 }
129 }
130 }
131 }
132
133 /**
134 * Copy commit info to clipboard when clicked
135 */
136 private copyCommitInfo(event: MouseEvent): void {
137 event.preventDefault();
138 event.stopPropagation();
139
140 if (!this.lastCommit) return;
141
142 const textToCopy =
143 this.lastCommit.pushedBranch || this.lastCommit.hash.substring(0, 8);
144
145 navigator.clipboard
146 .writeText(textToCopy)
147 .then(() => {
148 this.lastCommitCopied = true;
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700149 // Reset the copied state after 1.5 seconds
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000150 setTimeout(() => {
151 this.lastCommitCopied = false;
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700152 }, 1500);
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000153 })
154 .catch((err) => {
155 console.error("Failed to copy commit info:", err);
156 });
157 }
158
Philip Zeyligerd1402952025-04-23 03:54:37 +0000159 formatHostname() {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000160 // Only display outside hostname
Philip Zeyliger18532b22025-04-23 21:11:46 +0000161 const outsideHostname = this.state?.outside_hostname;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000162
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000163 if (!outsideHostname) {
Philip Zeyligerd1402952025-04-23 03:54:37 +0000164 return this.state?.hostname;
165 }
166
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000167 return outsideHostname;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000168 }
169
170 formatWorkingDir() {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000171 // Only display outside working directory
Philip Zeyliger18532b22025-04-23 21:11:46 +0000172 const outsideWorkingDir = this.state?.outside_working_dir;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000173
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000174 if (!outsideWorkingDir) {
Philip Zeyligerd1402952025-04-23 03:54:37 +0000175 return this.state?.working_dir;
176 }
177
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000178 return outsideWorkingDir;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000179 }
180
181 getHostnameTooltip() {
Philip Zeyliger18532b22025-04-23 21:11:46 +0000182 const outsideHostname = this.state?.outside_hostname;
183 const insideHostname = this.state?.inside_hostname;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000184
185 if (
Philip Zeyliger18532b22025-04-23 21:11:46 +0000186 !outsideHostname ||
187 !insideHostname ||
188 outsideHostname === insideHostname
Philip Zeyligerd1402952025-04-23 03:54:37 +0000189 ) {
190 return "";
191 }
192
Philip Zeyliger18532b22025-04-23 21:11:46 +0000193 return `Outside: ${outsideHostname}, Inside: ${insideHostname}`;
194 }
195
196 getWorkingDirTooltip() {
197 const outsideWorkingDir = this.state?.outside_working_dir;
198 const insideWorkingDir = this.state?.inside_working_dir;
199
200 if (
201 !outsideWorkingDir ||
202 !insideWorkingDir ||
203 outsideWorkingDir === insideWorkingDir
204 ) {
205 return "";
206 }
207
208 return `Outside: ${outsideWorkingDir}, Inside: ${insideWorkingDir}`;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000209 }
210
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000211 copyToClipboard(text: string) {
212 navigator.clipboard
213 .writeText(text)
214 .then(() => {
215 // Could add a temporary success indicator here
216 })
217 .catch((err) => {
218 console.error("Could not copy text: ", err);
219 });
220 }
221
222 getSSHHostname() {
philip.zeyliger8773e682025-06-11 21:36:21 -0700223 // Use the ssh_connection_string from the state if available, otherwise fall back to generating it
224 return (
225 this.state?.ssh_connection_string || `sketch-${this.state?.session_id}`
226 );
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000227 }
228
philip.zeyligere8da7af2025-06-12 14:24:28 -0700229 getSSHConnectionString() {
230 // Return the connection string for VS Code remote SSH
231 const connectionString =
232 this.state?.ssh_connection_string || `sketch-${this.state?.session_id}`;
233 // If the connection string already contains user@, use it as-is
234 // Otherwise prepend root@ for VS Code remote SSH
235 if (connectionString.includes("@")) {
236 return connectionString;
237 } else {
238 return `root@${connectionString}`;
239 }
240 }
241
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700242 /**
243 * Get sorted ports (by port number) from state, filtering out ports < 1024
244 */
245 getSortedPorts(): Port[] {
246 if (!this.state?.open_ports) {
247 return [];
248 }
249 return [...this.state.open_ports]
Autoformattere48f2bb2025-07-04 04:15:26 +0000250 .filter((port) => port.port >= 1024)
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700251 .sort((a, b) => a.port - b.port);
252 }
253
254 /**
255 * Generate URL for a port based on skaband_addr or localhost
256 */
257 getPortUrl(port: number): string {
258 if (this.state?.skaband_addr) {
259 // Use skaband proxy pattern: skabandaddr/proxy/<sessionId>/<port>
260 return `${this.state.skaband_addr}/proxy/${this.state.session_id}/${port}`;
261 } else {
262 // Use localhost pattern: http://p{port}.localhost:{sketch_port}
263 // We need to extract the port from the current URL
Autoformattere48f2bb2025-07-04 04:15:26 +0000264 const currentPort = window.location.port || "80";
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700265 return `http://p${port}.localhost:${currentPort}`;
266 }
267 }
268
269 /**
270 * Handle port link clicks
271 *
272 * TODO: Whereas Chrome resolves *.localhost as localhost,
273 * Safari does not. Ideally, if skaband_addr is empty, we
274 * could do a quick "fetch(p${port}.localhost)", and, if it
275 * doesn't work at all, we could show the user a modal explaining
276 * to use /etc/hosts. But, anyway, this would be nice but isn't done.
277 */
278 onPortClick(port: number, event: MouseEvent): void {
279 event.preventDefault();
280 event.stopPropagation();
281 const url = this.getPortUrl(port);
Autoformattere48f2bb2025-07-04 04:15:26 +0000282 window.open(url, "_blank");
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700283 }
284
285 /**
286 * Show more ports popup
287 */
288 private _showMorePorts(event: MouseEvent): void {
289 event.preventDefault();
290 event.stopPropagation();
291 this.showPortsPopup = !this.showPortsPopup;
292 this.requestUpdate();
293 }
294
295 /**
296 * Update port tracking and highlight newly opened ports
297 */
298 public updatePortInfo(newPorts: Port[]): void {
Autoformattere48f2bb2025-07-04 04:15:26 +0000299 const currentPorts = newPorts.filter((port) => port.port >= 1024);
300 const previousPortNumbers = new Set(this.previousPorts.map((p) => p.port));
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700301
302 // Find newly opened ports
Autoformattere48f2bb2025-07-04 04:15:26 +0000303 const newlyOpenedPorts = currentPorts.filter(
304 (port) => !previousPortNumbers.has(port.port),
305 );
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700306
307 if (newlyOpenedPorts.length > 0) {
308 // Add newly opened ports to highlighted set
Autoformattere48f2bb2025-07-04 04:15:26 +0000309 newlyOpenedPorts.forEach((port) => {
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700310 this.highlightedPorts.add(port.port);
311 });
312
313 // Remove highlights after animation completes
314 setTimeout(() => {
Autoformattere48f2bb2025-07-04 04:15:26 +0000315 newlyOpenedPorts.forEach((port) => {
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700316 this.highlightedPorts.delete(port.port);
317 });
318 this.requestUpdate();
319 }, 1500);
320 }
321
322 this.previousPorts = [...currentPorts];
323 this.requestUpdate();
324 }
325
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000326 // Format GitHub repository URL to org/repo format
327 formatGitHubRepo(url) {
328 if (!url) return null;
329
330 // Common GitHub URL patterns
331 const patterns = [
332 // HTTPS URLs
Sean McCulloughc7c2cc12025-06-13 03:21:18 +0000333 /https:\/\/github\.com\/([^/]+)\/([^/\s]+?)(?:\.git)?$/,
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000334 // SSH URLs
Sean McCulloughc7c2cc12025-06-13 03:21:18 +0000335 /git@github\.com:([^/]+)\/([^/\s]+?)(?:\.git)?$/,
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000336 // Git protocol
Sean McCulloughc7c2cc12025-06-13 03:21:18 +0000337 /git:\/\/github\.com\/([^/]+)\/([^/\s]+?)(?:\.git)?$/,
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000338 ];
339
340 for (const pattern of patterns) {
341 const match = url.match(pattern);
342 if (match) {
343 return {
344 formatted: `${match[1]}/${match[2]}`,
345 url: `https://github.com/${match[1]}/${match[2]}`,
philip.zeyliger6d3de482025-06-10 19:38:14 -0700346 owner: match[1],
347 repo: match[2],
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000348 };
349 }
350 }
351
352 return null;
353 }
354
philip.zeyliger6d3de482025-06-10 19:38:14 -0700355 // Generate GitHub branch URL if linking is enabled
356 getGitHubBranchLink(branchName) {
357 if (!this.state?.link_to_github || !branchName) {
358 return null;
359 }
360
361 const github = this.formatGitHubRepo(this.state?.git_origin);
362 if (!github) {
363 return null;
364 }
365
366 return `https://github.com/${github.owner}/${github.repo}/tree/${branchName}`;
367 }
368
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000369 renderSSHSection() {
370 // Only show SSH section if we're in a Docker container and have session ID
371 if (!this.state?.session_id) {
372 return html``;
373 }
374
philip.zeyliger26bc6592025-06-30 20:15:30 -0700375 const _sshHost = this.getSSHHostname();
philip.zeyligere8da7af2025-06-12 14:24:28 -0700376 const sshConnectionString = this.getSSHConnectionString();
377 const sshCommand = `ssh ${sshConnectionString}`;
378 const vscodeCommand = `code --remote ssh-remote+${sshConnectionString} /app -n`;
379 const vscodeURL = `vscode://vscode-remote/ssh-remote+${sshConnectionString}/app?windowId=_blank`;
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000380
381 if (!this.state?.ssh_available) {
382 return html`
banksean3eaa4332025-07-19 02:19:06 +0000383 <div
384 class="mt-2.5 pt-2.5 border-t border-gray-300 dark:border-gray-600"
385 >
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000386 <h3>Connect to Container</h3>
Sean McCullough7e36a042025-06-25 08:45:18 +0000387 <div
banksean3eaa4332025-07-19 02:19:06 +0000388 class="bg-orange-50 dark:bg-orange-900 border-l-4 border-orange-500 dark:border-orange-400 p-3 mt-2 text-xs text-orange-800 dark:text-orange-200"
Sean McCullough7e36a042025-06-25 08:45:18 +0000389 >
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000390 SSH connections are not available:
391 ${this.state?.ssh_error || "SSH configuration is missing"}
392 </div>
393 </div>
394 `;
395 }
396
397 return html`
banksean3eaa4332025-07-19 02:19:06 +0000398 <div class="mt-2.5 pt-2.5 border-t border-gray-300 dark:border-gray-600">
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000399 <h3>Connect to Container</h3>
Sean McCullough7e36a042025-06-25 08:45:18 +0000400 <div class="flex items-center mb-2 gap-2.5">
401 <div
banksean3eaa4332025-07-19 02:19:06 +0000402 class="font-mono text-xs bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded border border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100 flex-grow"
Sean McCullough7e36a042025-06-25 08:45:18 +0000403 >
404 ${sshCommand}
405 </div>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000406 <button
banksean3eaa4332025-07-19 02:19:06 +0000407 class="bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded px-1.5 py-0.5 text-xs text-gray-900 dark:text-gray-100 cursor-pointer transition-colors hover:bg-gray-200 dark:hover:bg-gray-600"
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000408 @click=${() => this.copyToClipboard(sshCommand)}
409 >
410 Copy
411 </button>
412 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000413 <div class="flex items-center mb-2 gap-2.5">
414 <div
banksean3eaa4332025-07-19 02:19:06 +0000415 class="font-mono text-xs bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded border border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100 flex-grow"
Sean McCullough7e36a042025-06-25 08:45:18 +0000416 >
417 ${vscodeCommand}
418 </div>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000419 <button
banksean3eaa4332025-07-19 02:19:06 +0000420 class="bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded px-1.5 py-0.5 text-xs text-gray-900 dark:text-gray-100 cursor-pointer transition-colors hover:bg-gray-200 dark:hover:bg-gray-600"
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000421 @click=${() => this.copyToClipboard(vscodeCommand)}
422 >
423 Copy
424 </button>
425 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000426 <div class="flex items-center mb-2 gap-2.5">
427 <a
428 href="${vscodeURL}"
429 class="text-white no-underline bg-blue-500 px-2 py-1 rounded flex items-center gap-1.5 text-xs transition-colors hover:bg-blue-800"
430 title="${vscodeURL}"
431 >
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000432 <svg
Sean McCullough7e36a042025-06-25 08:45:18 +0000433 class="w-4 h-4"
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000434 xmlns="http://www.w3.org/2000/svg"
435 viewBox="0 0 24 24"
436 fill="none"
437 stroke="white"
438 stroke-width="2"
439 stroke-linecap="round"
440 stroke-linejoin="round"
441 >
442 <path
443 d="M16.5 9.4 7.55 4.24a.35.35 0 0 0-.41.01l-1.23.93a.35.35 0 0 0-.14.29v13.04c0 .12.07.23.17.29l1.24.93c.13.1.31.09.43-.01L16.5 14.6l-6.39 4.82c-.16.12-.38.12-.55.01l-1.33-1.01a.35.35 0 0 1-.14-.28V5.88c0-.12.07-.23.18-.29l1.23-.93c.14-.1.32-.1.46 0l6.54 4.92-6.54 4.92c-.14.1-.32.1-.46 0l-1.23-.93a.35.35 0 0 1-.18-.29V5.88c0-.12.07-.23.17-.29l1.33-1.01c.16-.12.39-.11.55.01l6.39 4.81z"
444 />
445 </svg>
446 <span>Open in VSCode</span>
447 </a>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000448 </div>
449 </div>
450 `;
451 }
452
Sean McCullough86b56862025-04-18 13:04:03 -0700453 render() {
454 return html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000455 <div class="flex items-center relative">
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700456 <!-- Main visible info in two columns - github/hostname/dir and last commit -->
Sean McCullough7e36a042025-06-25 08:45:18 +0000457 <div class="flex flex-wrap gap-2 px-2.5 py-1 flex-1">
Sean McCullough49577492025-06-26 17:13:28 -0700458 <div class="flex gap-2.5 w-full">
Sean McCullough7e36a042025-06-25 08:45:18 +0000459 <!-- First column: GitHub repo (or hostname) and working dir -->
460 <div class="flex flex-col gap-0.5">
461 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
462 ${(() => {
463 const github = this.formatGitHubRepo(this.state?.git_origin);
464 if (github) {
465 return html`
466 <a
467 href="${github.url}"
468 target="_blank"
469 rel="noopener noreferrer"
470 class="github-link text-blue-600 no-underline hover:underline"
471 title="${this.state?.git_origin}"
472 >
473 ${github.formatted}
474 </a>
475 `;
476 } else {
477 return html`
478 <span
479 id="hostname"
480 class="text-xs font-semibold break-all cursor-default"
481 title="${this.getHostnameTooltip()}"
482 >
483 ${this.formatHostname()}
484 </span>
485 `;
486 }
487 })()}
488 </div>
489 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
490 <span
491 id="workingDir"
492 class="text-xs font-semibold break-all cursor-default"
493 title="${this.getWorkingDirTooltip()}"
494 >
495 ${this.formatWorkingDir()}
496 </span>
497 </div>
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000498 </div>
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000499
Sean McCullough7e36a042025-06-25 08:45:18 +0000500 <!-- Second column: Last Commit -->
501 <div class="flex flex-col gap-0.5 justify-start">
502 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
503 <span class="text-xs text-gray-600 font-medium"
504 >Last Commit</span
505 >
506 </div>
507 <div
508 class="flex items-center whitespace-nowrap mr-2.5 text-xs cursor-pointer relative pt-0 last-commit-main hover:text-blue-600"
509 @click=${(e: MouseEvent) => this.copyCommitInfo(e)}
510 title="Click to copy"
511 >
512 ${this.lastCommit
513 ? this.lastCommit.pushedBranch
514 ? (() => {
515 const githubLink = this.getGitHubBranchLink(
516 this.lastCommit.pushedBranch,
517 );
518 return html`
519 <div class="flex items-center gap-1.5">
520 <span
521 class="text-green-600 font-mono text-xs whitespace-nowrap overflow-hidden text-ellipsis"
522 title="Click to copy: ${this.lastCommit
523 .pushedBranch}"
524 @click=${(e) => this.copyCommitInfo(e)}
525 >${this.lastCommit.pushedBranch}</span
526 >
527 <span
528 class="ml-1 opacity-70 flex items-center hover:opacity-100"
529 >
530 ${this.lastCommitCopied
531 ? html`<svg
532 xmlns="http://www.w3.org/2000/svg"
533 width="16"
534 height="16"
535 viewBox="0 0 24 24"
536 fill="none"
537 stroke="currentColor"
538 stroke-width="2"
539 stroke-linecap="round"
540 stroke-linejoin="round"
541 class="align-middle"
542 >
543 <path d="M20 6L9 17l-5-5"></path>
544 </svg>`
545 : html`<svg
546 xmlns="http://www.w3.org/2000/svg"
547 width="16"
548 height="16"
549 viewBox="0 0 24 24"
550 fill="none"
551 stroke="currentColor"
552 stroke-width="2"
553 stroke-linecap="round"
554 stroke-linejoin="round"
555 class="align-middle"
556 >
557 <rect
558 x="9"
559 y="9"
560 width="13"
561 height="13"
562 rx="2"
563 ry="2"
564 ></rect>
565 <path
566 d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
567 ></path>
568 </svg>`}
569 </span>
570 ${githubLink
571 ? html`<a
572 href="${githubLink}"
573 target="_blank"
574 rel="noopener noreferrer"
575 class="text-gray-600 no-underline flex items-center transition-colors hover:text-blue-600"
576 title="Open ${this.lastCommit
577 .pushedBranch} on GitHub"
578 @click=${(e) => e.stopPropagation()}
philip.zeyliger6d3de482025-06-10 19:38:14 -0700579 >
Sean McCullough7e36a042025-06-25 08:45:18 +0000580 <svg
581 class="w-4 h-4"
582 viewBox="0 0 16 16"
583 width="16"
584 height="16"
585 >
586 <path
587 fill="currentColor"
588 d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
589 />
590 </svg>
591 </a>`
592 : ""}
593 </div>
594 `;
595 })()
596 : html`<span
597 class="text-gray-600 font-mono text-xs whitespace-nowrap overflow-hidden text-ellipsis"
598 >${this.lastCommit.hash.substring(0, 8)}</span
599 >`
600 : html`<span class="text-gray-500 italic text-xs">N/A</span>`}
601 </div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000602 </div>
603 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700604 </div>
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000605
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700606 <!-- Ports section -->
607 ${(() => {
608 const ports = this.getSortedPorts();
609 if (ports.length === 0) {
610 return html``;
611 }
612 const displayPorts = ports.slice(0, 2);
613 const remainingPorts = ports.slice(2);
614 return html`
615 <div class="flex items-center gap-1 ml-2">
Autoformattere48f2bb2025-07-04 04:15:26 +0000616 ${displayPorts.map(
617 (port) => html`
618 <button
619 class="text-xs bg-gray-100 hover:bg-gray-200 px-1.5 py-0.5 rounded border border-gray-300 cursor-pointer transition-colors flex items-center gap-1 ${this.highlightedPorts.has(
620 port.port,
621 )
622 ? "pulse-custom"
623 : ""}"
624 @click=${(e: MouseEvent) => this.onPortClick(port.port, e)}
625 title="Open ${port.process} on port ${port.port}"
626 >
627 <span>${port.process}(${port.port})</span>
628 <span>🔗</span>
629 </button>
630 `,
631 )}
632 ${remainingPorts.length > 0
633 ? html`
634 <button
635 class="text-xs bg-gray-100 hover:bg-gray-200 px-1.5 py-0.5 rounded border border-gray-300 cursor-pointer transition-colors ${remainingPorts.some(
636 (port) => this.highlightedPorts.has(port.port),
637 )
638 ? "pulse-custom"
639 : ""}"
640 @click=${(e: MouseEvent) => this._showMorePorts(e)}
641 title="Show ${remainingPorts.length} more ports"
642 >
643 +${remainingPorts.length}
644 </button>
645 `
646 : html``}
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700647 </div>
648 `;
649 })()}
650
Philip Zeyliger254c49f2025-07-17 17:26:24 -0700651 <!-- Push button -->
652 <sketch-push-button class="ml-2"></sketch-push-button>
653
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000654 <!-- Info toggle button -->
655 <button
Sean McCullough7e36a042025-06-25 08:45:18 +0000656 class="info-toggle ml-2 w-6 h-6 rounded-full flex items-center justify-center ${this
657 .showDetails
658 ? "bg-blue-500 text-white border-blue-600"
banksean3eaa4332025-07-19 02:19:06 +0000659 : "bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 border-gray-300 dark:border-gray-600"} border cursor-pointer font-bold italic transition-all hover:${this
Sean McCullough7e36a042025-06-25 08:45:18 +0000660 .showDetails
661 ? "bg-blue-600"
banksean3eaa4332025-07-19 02:19:06 +0000662 : "bg-gray-200 dark:bg-gray-600"}"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000663 @click=${this._toggleInfoDetails}
664 title="Show/hide details"
665 >
666 i
667 </button>
668
669 <!-- Expanded info panel -->
Sean McCullough7e36a042025-06-25 08:45:18 +0000670 <div
671 class="${this.showDetails
672 ? "block"
banksean3eaa4332025-07-19 02:19:06 +0000673 : "hidden"} absolute min-w-max top-full z-100 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg p-4 shadow-lg mt-1.5"
Philip Zeyliger254c49f2025-07-17 17:26:24 -0700674 style="left: 50%; transform: translateX(-50%);"
Sean McCullough7e36a042025-06-25 08:45:18 +0000675 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000676 <!-- Last Commit section moved to main grid -->
677
Sean McCullough7e36a042025-06-25 08:45:18 +0000678 <div
Sean McCulloughc37e0662025-07-03 08:46:21 -0700679 class="grid gap-2 mt-2.5"
680 style="grid-template-columns: auto auto"
Sean McCullough7e36a042025-06-25 08:45:18 +0000681 >
682 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
banksean3eaa4332025-07-19 02:19:06 +0000683 <span
684 class="text-xs text-gray-600 dark:text-gray-400 mr-1 font-medium"
Sean McCullough7e36a042025-06-25 08:45:18 +0000685 >Commit:</span
686 >
banksean3eaa4332025-07-19 02:19:06 +0000687 <span
688 id="initialCommit"
689 class="text-xs font-semibold break-all text-gray-900 dark:text-gray-100"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000690 >${this.state?.initial_commit?.substring(0, 8)}</span
691 >
692 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000693 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
banksean3eaa4332025-07-19 02:19:06 +0000694 <span
695 class="text-xs text-gray-600 dark:text-gray-400 mr-1 font-medium"
696 >Msgs:</span
697 >
698 <span
699 id="messageCount"
700 class="text-xs font-semibold break-all text-gray-900 dark:text-gray-100"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000701 >${this.state?.message_count}</span
702 >
703 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000704 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
banksean3eaa4332025-07-19 02:19:06 +0000705 <span
706 class="text-xs text-gray-600 dark:text-gray-400 mr-1 font-medium"
Sean McCullough7e36a042025-06-25 08:45:18 +0000707 >Session ID:</span
708 >
banksean3eaa4332025-07-19 02:19:06 +0000709 <span
710 id="sessionId"
711 class="text-xs font-semibold break-all text-gray-900 dark:text-gray-100"
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000712 >${this.state?.session_id || "N/A"}</span
713 >
714 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000715 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
banksean3eaa4332025-07-19 02:19:06 +0000716 <span
717 class="text-xs text-gray-600 dark:text-gray-400 mr-1 font-medium"
Sean McCullough7e36a042025-06-25 08:45:18 +0000718 >Hostname:</span
719 >
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700720 <span
721 id="hostnameDetail"
banksean3eaa4332025-07-19 02:19:06 +0000722 class="text-xs font-semibold break-all cursor-default text-gray-900 dark:text-gray-100"
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700723 title="${this.getHostnameTooltip()}"
724 >
725 ${this.formatHostname()}
726 </span>
727 </div>
Philip Zeyliger72318392025-05-14 02:56:07 +0000728 ${this.state?.agent_state
729 ? html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000730 <div
731 class="flex items-center whitespace-nowrap mr-2.5 text-xs"
732 >
banksean3eaa4332025-07-19 02:19:06 +0000733 <span
734 class="text-xs text-gray-600 dark:text-gray-400 mr-1 font-medium"
Sean McCullough7e36a042025-06-25 08:45:18 +0000735 >Agent State:</span
736 >
737 <span
738 id="agentState"
banksean3eaa4332025-07-19 02:19:06 +0000739 class="text-xs font-semibold break-all text-gray-900 dark:text-gray-100"
Philip Zeyliger72318392025-05-14 02:56:07 +0000740 >${this.state?.agent_state}</span
741 >
742 </div>
743 `
744 : ""}
Sean McCullough7e36a042025-06-25 08:45:18 +0000745 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
banksean3eaa4332025-07-19 02:19:06 +0000746 <span
747 class="text-xs text-gray-600 dark:text-gray-400 mr-1 font-medium"
Sean McCullough7e36a042025-06-25 08:45:18 +0000748 >Input tokens:</span
749 >
banksean3eaa4332025-07-19 02:19:06 +0000750 <span
751 id="inputTokens"
752 class="text-xs font-semibold break-all text-gray-900 dark:text-gray-100"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000753 >${formatNumber(
754 (this.state?.total_usage?.input_tokens || 0) +
755 (this.state?.total_usage?.cache_read_input_tokens || 0) +
756 (this.state?.total_usage?.cache_creation_input_tokens || 0),
757 )}</span
758 >
759 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000760 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
banksean3eaa4332025-07-19 02:19:06 +0000761 <span
762 class="text-xs text-gray-600 dark:text-gray-400 mr-1 font-medium"
banksean5ab8fb82025-07-09 12:34:55 -0700763 >Context Window:</span
Sean McCulloughc37e0662025-07-03 08:46:21 -0700764 >
banksean3eaa4332025-07-19 02:19:06 +0000765 <span
766 id="contextWindow"
767 class="text-xs font-semibold break-all text-gray-900 dark:text-gray-100"
Sean McCulloughc37e0662025-07-03 08:46:21 -0700768 >${formatNumber(
769 (this.latestUsage?.input_tokens || 0) +
770 (this.latestUsage?.cache_read_input_tokens || 0) +
771 (this.latestUsage?.cache_creation_input_tokens || 0),
banksean5ab8fb82025-07-09 12:34:55 -0700772 )}/${formatNumber(this.state?.token_context_window || 0)}</span
Sean McCulloughc37e0662025-07-03 08:46:21 -0700773 >
774 </div>
Josh Bleecher Snyder4571fd62025-07-25 16:56:02 +0000775 ${this.state?.model
776 ? html`
777 <div
778 class="flex items-center whitespace-nowrap mr-2.5 text-xs"
779 >
780 <span
781 class="text-xs text-gray-600 dark:text-gray-400 mr-1 font-medium"
782 >Model:</span
783 >
784 <span
785 id="modelName"
786 class="text-xs font-semibold break-all text-gray-900 dark:text-gray-100"
787 >${this.state?.model}</span
788 >
789 </div>
790 `
791 : ""}
Sean McCulloughc37e0662025-07-03 08:46:21 -0700792 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
banksean3eaa4332025-07-19 02:19:06 +0000793 <span
794 class="text-xs text-gray-600 dark:text-gray-400 mr-1 font-medium"
Sean McCullough7e36a042025-06-25 08:45:18 +0000795 >Output tokens:</span
796 >
banksean3eaa4332025-07-19 02:19:06 +0000797 <span
798 id="outputTokens"
799 class="text-xs font-semibold break-all text-gray-900 dark:text-gray-100"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000800 >${formatNumber(this.state?.total_usage?.output_tokens)}</span
801 >
802 </div>
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700803 ${(this.state?.total_usage?.total_cost_usd || 0) > 0
804 ? html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000805 <div
806 class="flex items-center whitespace-nowrap mr-2.5 text-xs"
807 >
banksean3eaa4332025-07-19 02:19:06 +0000808 <span
809 class="text-xs text-gray-600 dark:text-gray-400 mr-1 font-medium"
Sean McCullough7e36a042025-06-25 08:45:18 +0000810 >Total cost:</span
811 >
banksean3eaa4332025-07-19 02:19:06 +0000812 <span
813 id="totalCost"
814 class="text-xs font-semibold break-all text-gray-900 dark:text-gray-100"
philip.zeyliger26bc6592025-06-30 20:15:30 -0700815 >$${(
816 this.state?.total_usage?.total_cost_usd ?? 0
817 ).toFixed(2)}</span
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700818 >
819 </div>
820 `
821 : ""}
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000822 <div
banksean3eaa4332025-07-19 02:19:06 +0000823 class="flex items-center whitespace-nowrap mr-2.5 text-xs col-span-full mt-1.5 border-t border-gray-300 dark:border-gray-600 pt-1.5"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000824 >
Josh Bleecher Snyder4a370aa2025-07-28 23:19:48 +0000825 <a href="debug/logs" class="text-blue-600">Logs</a> (<a
Sean McCullough7e36a042025-06-25 08:45:18 +0000826 href="download"
827 class="text-blue-600"
828 >Download</a
829 >)
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000830 </div>
831 </div>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000832
833 <!-- SSH Connection Information -->
834 ${this.renderSSHSection()}
Sean McCullough86b56862025-04-18 13:04:03 -0700835 </div>
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700836
837 <!-- Ports popup -->
838 <div
839 class="${this.showPortsPopup
840 ? "block"
841 : "hidden"} absolute min-w-max top-full right-0 z-20 bg-white rounded-lg p-3 shadow-lg mt-1.5 border border-gray-200"
842 >
843 <h3 class="text-sm font-semibold mb-2">Open Ports</h3>
844 <div class="flex flex-col gap-1">
Autoformattere48f2bb2025-07-04 04:15:26 +0000845 ${this.getSortedPorts().map(
846 (port) => html`
847 <button
848 class="text-xs bg-gray-100 hover:bg-gray-200 px-2 py-1 rounded border border-gray-300 cursor-pointer transition-colors flex items-center gap-2 justify-between"
849 @click=${(e: MouseEvent) => this.onPortClick(port.port, e)}
850 title="Open ${port.process} on port ${port.port}"
851 >
852 <span>${port.process}(${port.port})</span>
853 <span>🔗</span>
854 </button>
855 `,
856 )}
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700857 </div>
858 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700859 </div>
860 `;
861 }
862}
863
864declare global {
865 interface HTMLElementTagNameMap {
866 "sketch-container-status": SketchContainerStatus;
867 }
868}