blob: bd9524b3690a3f0a958ffc1ae6e0130644be0d9e [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`
Sean McCullough7e36a042025-06-25 08:45:18 +0000383 <div class="mt-2.5 pt-2.5 border-t border-gray-300">
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000384 <h3>Connect to Container</h3>
Sean McCullough7e36a042025-06-25 08:45:18 +0000385 <div
386 class="bg-orange-50 border-l-4 border-orange-500 p-3 mt-2 text-xs text-orange-800"
387 >
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000388 SSH connections are not available:
389 ${this.state?.ssh_error || "SSH configuration is missing"}
390 </div>
391 </div>
392 `;
393 }
394
395 return html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000396 <div class="mt-2.5 pt-2.5 border-t border-gray-300">
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000397 <h3>Connect to Container</h3>
Sean McCullough7e36a042025-06-25 08:45:18 +0000398 <div class="flex items-center mb-2 gap-2.5">
399 <div
400 class="font-mono text-xs bg-gray-100 px-2 py-1 rounded border border-gray-300 flex-grow"
401 >
402 ${sshCommand}
403 </div>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000404 <button
Sean McCullough7e36a042025-06-25 08:45:18 +0000405 class="bg-gray-100 border border-gray-300 rounded px-1.5 py-0.5 text-xs cursor-pointer transition-colors hover:bg-gray-200"
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000406 @click=${() => this.copyToClipboard(sshCommand)}
407 >
408 Copy
409 </button>
410 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000411 <div class="flex items-center mb-2 gap-2.5">
412 <div
413 class="font-mono text-xs bg-gray-100 px-2 py-1 rounded border border-gray-300 flex-grow"
414 >
415 ${vscodeCommand}
416 </div>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000417 <button
Sean McCullough7e36a042025-06-25 08:45:18 +0000418 class="bg-gray-100 border border-gray-300 rounded px-1.5 py-0.5 text-xs cursor-pointer transition-colors hover:bg-gray-200"
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000419 @click=${() => this.copyToClipboard(vscodeCommand)}
420 >
421 Copy
422 </button>
423 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000424 <div class="flex items-center mb-2 gap-2.5">
425 <a
426 href="${vscodeURL}"
427 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"
428 title="${vscodeURL}"
429 >
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000430 <svg
Sean McCullough7e36a042025-06-25 08:45:18 +0000431 class="w-4 h-4"
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000432 xmlns="http://www.w3.org/2000/svg"
433 viewBox="0 0 24 24"
434 fill="none"
435 stroke="white"
436 stroke-width="2"
437 stroke-linecap="round"
438 stroke-linejoin="round"
439 >
440 <path
441 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"
442 />
443 </svg>
444 <span>Open in VSCode</span>
445 </a>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000446 </div>
447 </div>
448 `;
449 }
450
Sean McCullough86b56862025-04-18 13:04:03 -0700451 render() {
452 return html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000453 <div class="flex items-center relative">
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700454 <!-- Main visible info in two columns - github/hostname/dir and last commit -->
Sean McCullough7e36a042025-06-25 08:45:18 +0000455 <div class="flex flex-wrap gap-2 px-2.5 py-1 flex-1">
Sean McCullough49577492025-06-26 17:13:28 -0700456 <div class="flex gap-2.5 w-full">
Sean McCullough7e36a042025-06-25 08:45:18 +0000457 <!-- First column: GitHub repo (or hostname) and working dir -->
458 <div class="flex flex-col gap-0.5">
459 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
460 ${(() => {
461 const github = this.formatGitHubRepo(this.state?.git_origin);
462 if (github) {
463 return html`
464 <a
465 href="${github.url}"
466 target="_blank"
467 rel="noopener noreferrer"
468 class="github-link text-blue-600 no-underline hover:underline"
469 title="${this.state?.git_origin}"
470 >
471 ${github.formatted}
472 </a>
473 `;
474 } else {
475 return html`
476 <span
477 id="hostname"
478 class="text-xs font-semibold break-all cursor-default"
479 title="${this.getHostnameTooltip()}"
480 >
481 ${this.formatHostname()}
482 </span>
483 `;
484 }
485 })()}
486 </div>
487 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
488 <span
489 id="workingDir"
490 class="text-xs font-semibold break-all cursor-default"
491 title="${this.getWorkingDirTooltip()}"
492 >
493 ${this.formatWorkingDir()}
494 </span>
495 </div>
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000496 </div>
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000497
Sean McCullough7e36a042025-06-25 08:45:18 +0000498 <!-- Second column: Last Commit -->
499 <div class="flex flex-col gap-0.5 justify-start">
500 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
501 <span class="text-xs text-gray-600 font-medium"
502 >Last Commit</span
503 >
504 </div>
505 <div
506 class="flex items-center whitespace-nowrap mr-2.5 text-xs cursor-pointer relative pt-0 last-commit-main hover:text-blue-600"
507 @click=${(e: MouseEvent) => this.copyCommitInfo(e)}
508 title="Click to copy"
509 >
510 ${this.lastCommit
511 ? this.lastCommit.pushedBranch
512 ? (() => {
513 const githubLink = this.getGitHubBranchLink(
514 this.lastCommit.pushedBranch,
515 );
516 return html`
517 <div class="flex items-center gap-1.5">
518 <span
519 class="text-green-600 font-mono text-xs whitespace-nowrap overflow-hidden text-ellipsis"
520 title="Click to copy: ${this.lastCommit
521 .pushedBranch}"
522 @click=${(e) => this.copyCommitInfo(e)}
523 >${this.lastCommit.pushedBranch}</span
524 >
525 <span
526 class="ml-1 opacity-70 flex items-center hover:opacity-100"
527 >
528 ${this.lastCommitCopied
529 ? html`<svg
530 xmlns="http://www.w3.org/2000/svg"
531 width="16"
532 height="16"
533 viewBox="0 0 24 24"
534 fill="none"
535 stroke="currentColor"
536 stroke-width="2"
537 stroke-linecap="round"
538 stroke-linejoin="round"
539 class="align-middle"
540 >
541 <path d="M20 6L9 17l-5-5"></path>
542 </svg>`
543 : html`<svg
544 xmlns="http://www.w3.org/2000/svg"
545 width="16"
546 height="16"
547 viewBox="0 0 24 24"
548 fill="none"
549 stroke="currentColor"
550 stroke-width="2"
551 stroke-linecap="round"
552 stroke-linejoin="round"
553 class="align-middle"
554 >
555 <rect
556 x="9"
557 y="9"
558 width="13"
559 height="13"
560 rx="2"
561 ry="2"
562 ></rect>
563 <path
564 d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
565 ></path>
566 </svg>`}
567 </span>
568 ${githubLink
569 ? html`<a
570 href="${githubLink}"
571 target="_blank"
572 rel="noopener noreferrer"
573 class="text-gray-600 no-underline flex items-center transition-colors hover:text-blue-600"
574 title="Open ${this.lastCommit
575 .pushedBranch} on GitHub"
576 @click=${(e) => e.stopPropagation()}
philip.zeyliger6d3de482025-06-10 19:38:14 -0700577 >
Sean McCullough7e36a042025-06-25 08:45:18 +0000578 <svg
579 class="w-4 h-4"
580 viewBox="0 0 16 16"
581 width="16"
582 height="16"
583 >
584 <path
585 fill="currentColor"
586 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"
587 />
588 </svg>
589 </a>`
590 : ""}
591 </div>
592 `;
593 })()
594 : html`<span
595 class="text-gray-600 font-mono text-xs whitespace-nowrap overflow-hidden text-ellipsis"
596 >${this.lastCommit.hash.substring(0, 8)}</span
597 >`
598 : html`<span class="text-gray-500 italic text-xs">N/A</span>`}
599 </div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000600 </div>
601 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700602 </div>
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000603
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700604 <!-- Ports section -->
605 ${(() => {
606 const ports = this.getSortedPorts();
607 if (ports.length === 0) {
608 return html``;
609 }
610 const displayPorts = ports.slice(0, 2);
611 const remainingPorts = ports.slice(2);
612 return html`
613 <div class="flex items-center gap-1 ml-2">
Autoformattere48f2bb2025-07-04 04:15:26 +0000614 ${displayPorts.map(
615 (port) => html`
616 <button
617 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(
618 port.port,
619 )
620 ? "pulse-custom"
621 : ""}"
622 @click=${(e: MouseEvent) => this.onPortClick(port.port, e)}
623 title="Open ${port.process} on port ${port.port}"
624 >
625 <span>${port.process}(${port.port})</span>
626 <span>🔗</span>
627 </button>
628 `,
629 )}
630 ${remainingPorts.length > 0
631 ? html`
632 <button
633 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(
634 (port) => this.highlightedPorts.has(port.port),
635 )
636 ? "pulse-custom"
637 : ""}"
638 @click=${(e: MouseEvent) => this._showMorePorts(e)}
639 title="Show ${remainingPorts.length} more ports"
640 >
641 +${remainingPorts.length}
642 </button>
643 `
644 : html``}
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700645 </div>
646 `;
647 })()}
648
Philip Zeyliger254c49f2025-07-17 17:26:24 -0700649 <!-- Push button -->
650 <sketch-push-button class="ml-2"></sketch-push-button>
651
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000652 <!-- Info toggle button -->
653 <button
Sean McCullough7e36a042025-06-25 08:45:18 +0000654 class="info-toggle ml-2 w-6 h-6 rounded-full flex items-center justify-center ${this
655 .showDetails
656 ? "bg-blue-500 text-white border-blue-600"
657 : "bg-gray-100 text-gray-600 border-gray-300"} border cursor-pointer font-bold italic transition-all hover:${this
658 .showDetails
659 ? "bg-blue-600"
660 : "bg-gray-200"}"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000661 @click=${this._toggleInfoDetails}
662 title="Show/hide details"
663 >
664 i
665 </button>
666
667 <!-- Expanded info panel -->
Sean McCullough7e36a042025-06-25 08:45:18 +0000668 <div
669 class="${this.showDetails
670 ? "block"
Sean McCulloughc52a2502025-07-06 20:27:59 -0700671 : "hidden"} absolute min-w-max top-full z-100 bg-white rounded-lg p-4 shadow-lg mt-1.5"
Philip Zeyliger254c49f2025-07-17 17:26:24 -0700672 style="left: 50%; transform: translateX(-50%);"
Sean McCullough7e36a042025-06-25 08:45:18 +0000673 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000674 <!-- Last Commit section moved to main grid -->
675
Sean McCullough7e36a042025-06-25 08:45:18 +0000676 <div
Sean McCulloughc37e0662025-07-03 08:46:21 -0700677 class="grid gap-2 mt-2.5"
678 style="grid-template-columns: auto auto"
Sean McCullough7e36a042025-06-25 08:45:18 +0000679 >
680 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
681 <span class="text-xs text-gray-600 mr-1 font-medium"
682 >Commit:</span
683 >
684 <span id="initialCommit" class="text-xs font-semibold break-all"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000685 >${this.state?.initial_commit?.substring(0, 8)}</span
686 >
687 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000688 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
689 <span class="text-xs text-gray-600 mr-1 font-medium">Msgs:</span>
690 <span id="messageCount" class="text-xs font-semibold break-all"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000691 >${this.state?.message_count}</span
692 >
693 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000694 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
695 <span class="text-xs text-gray-600 mr-1 font-medium"
696 >Session ID:</span
697 >
698 <span id="sessionId" class="text-xs font-semibold break-all"
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000699 >${this.state?.session_id || "N/A"}</span
700 >
701 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000702 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
703 <span class="text-xs text-gray-600 mr-1 font-medium"
704 >Hostname:</span
705 >
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700706 <span
707 id="hostnameDetail"
Sean McCullough7e36a042025-06-25 08:45:18 +0000708 class="text-xs font-semibold break-all cursor-default"
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700709 title="${this.getHostnameTooltip()}"
710 >
711 ${this.formatHostname()}
712 </span>
713 </div>
Philip Zeyliger72318392025-05-14 02:56:07 +0000714 ${this.state?.agent_state
715 ? html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000716 <div
717 class="flex items-center whitespace-nowrap mr-2.5 text-xs"
718 >
719 <span class="text-xs text-gray-600 mr-1 font-medium"
720 >Agent State:</span
721 >
722 <span
723 id="agentState"
724 class="text-xs font-semibold break-all"
Philip Zeyliger72318392025-05-14 02:56:07 +0000725 >${this.state?.agent_state}</span
726 >
727 </div>
728 `
729 : ""}
Sean McCullough7e36a042025-06-25 08:45:18 +0000730 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
731 <span class="text-xs text-gray-600 mr-1 font-medium"
732 >Input tokens:</span
733 >
734 <span id="inputTokens" class="text-xs font-semibold break-all"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000735 >${formatNumber(
736 (this.state?.total_usage?.input_tokens || 0) +
737 (this.state?.total_usage?.cache_read_input_tokens || 0) +
738 (this.state?.total_usage?.cache_creation_input_tokens || 0),
739 )}</span
740 >
741 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000742 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
743 <span class="text-xs text-gray-600 mr-1 font-medium"
banksean5ab8fb82025-07-09 12:34:55 -0700744 >Context Window:</span
Sean McCulloughc37e0662025-07-03 08:46:21 -0700745 >
746 <span id="contextWindow" class="text-xs font-semibold break-all"
747 >${formatNumber(
748 (this.latestUsage?.input_tokens || 0) +
749 (this.latestUsage?.cache_read_input_tokens || 0) +
750 (this.latestUsage?.cache_creation_input_tokens || 0),
banksean5ab8fb82025-07-09 12:34:55 -0700751 )}/${formatNumber(this.state?.token_context_window || 0)}</span
Sean McCulloughc37e0662025-07-03 08:46:21 -0700752 >
753 </div>
754 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
755 <span class="text-xs text-gray-600 mr-1 font-medium"
Sean McCullough7e36a042025-06-25 08:45:18 +0000756 >Output tokens:</span
757 >
758 <span id="outputTokens" class="text-xs font-semibold break-all"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000759 >${formatNumber(this.state?.total_usage?.output_tokens)}</span
760 >
761 </div>
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700762 ${(this.state?.total_usage?.total_cost_usd || 0) > 0
763 ? html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000764 <div
765 class="flex items-center whitespace-nowrap mr-2.5 text-xs"
766 >
767 <span class="text-xs text-gray-600 mr-1 font-medium"
768 >Total cost:</span
769 >
770 <span id="totalCost" class="text-xs font-semibold break-all"
philip.zeyliger26bc6592025-06-30 20:15:30 -0700771 >$${(
772 this.state?.total_usage?.total_cost_usd ?? 0
773 ).toFixed(2)}</span
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700774 >
775 </div>
776 `
777 : ""}
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000778 <div
Sean McCullough7e36a042025-06-25 08:45:18 +0000779 class="flex items-center whitespace-nowrap mr-2.5 text-xs col-span-full mt-1.5 border-t border-gray-300 pt-1.5"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000780 >
Sean McCullough7e36a042025-06-25 08:45:18 +0000781 <a href="logs" class="text-blue-600">Logs</a> (<a
782 href="download"
783 class="text-blue-600"
784 >Download</a
785 >)
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000786 </div>
787 </div>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000788
789 <!-- SSH Connection Information -->
790 ${this.renderSSHSection()}
Sean McCullough86b56862025-04-18 13:04:03 -0700791 </div>
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700792
793 <!-- Ports popup -->
794 <div
795 class="${this.showPortsPopup
796 ? "block"
797 : "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"
798 >
799 <h3 class="text-sm font-semibold mb-2">Open Ports</h3>
800 <div class="flex flex-col gap-1">
Autoformattere48f2bb2025-07-04 04:15:26 +0000801 ${this.getSortedPorts().map(
802 (port) => html`
803 <button
804 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"
805 @click=${(e: MouseEvent) => this.onPortClick(port.port, e)}
806 title="Open ${port.process} on port ${port.port}"
807 >
808 <span>${port.process}(${port.port})</span>
809 <span>🔗</span>
810 </button>
811 `,
812 )}
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700813 </div>
814 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700815 </div>
816 `;
817 }
818}
819
820declare global {
821 interface HTMLElementTagNameMap {
822 "sketch-container-status": SketchContainerStatus;
823 }
824}