blob: be76eed3380c3eeeb594868c9d74d3d6e74517b1 [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";
Sean McCullough86b56862025-04-18 13:04:03 -07006
7@customElement("sketch-container-status")
Sean McCullough7e36a042025-06-25 08:45:18 +00008export class SketchContainerStatus extends SketchTailwindElement {
Sean McCullough86b56862025-04-18 13:04:03 -07009 // Header bar: Container status details
10
11 @property()
12 state: State;
13
Philip Zeyligere66db3e2025-04-27 15:40:39 +000014 @state()
15 showDetails: boolean = false;
16
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000017 @state()
18 lastCommit: { hash: string; pushedBranch?: string } | null = null;
19
20 @state()
21 lastCommitCopied: boolean = false;
22
Sean McCulloughc37e0662025-07-03 08:46:21 -070023 @state()
24 latestUsage: Usage | null = null;
25
Philip Zeyliger6dc90c02025-07-03 20:12:49 -070026 @state()
27 showPortsPopup: boolean = false;
28
29 @state()
30 previousPorts: Port[] = [];
31
32 @state()
33 highlightedPorts: Set<number> = new Set();
34
Sean McCullough7e36a042025-06-25 08:45:18 +000035 // CSS animations that can't be easily replaced with Tailwind
36 connectedCallback() {
37 super.connectedCallback();
38 // Add custom CSS animations to the document head if not already present
39 if (!document.querySelector("#container-status-animations")) {
40 const style = document.createElement("style");
41 style.id = "container-status-animations";
42 style.textContent = `
43 @keyframes pulse-custom {
44 0% { transform: scale(1); opacity: 1; }
45 50% { transform: scale(1.05); opacity: 0.8; }
46 100% { transform: scale(1); opacity: 1; }
47 }
48 .pulse-custom {
49 animation: pulse-custom 1.5s ease-in-out;
50 background-color: rgba(38, 132, 255, 0.1);
51 border-radius: 3px;
52 }
53 `;
54 document.head.appendChild(style);
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000055 }
Sean McCullough7e36a042025-06-25 08:45:18 +000056 }
Sean McCullough86b56862025-04-18 13:04:03 -070057
58 constructor() {
59 super();
Philip Zeyligere66db3e2025-04-27 15:40:39 +000060 this._toggleInfoDetails = this._toggleInfoDetails.bind(this);
61
62 // Close the info panel when clicking outside of it
63 document.addEventListener("click", (event) => {
64 if (this.showDetails && !this.contains(event.target as Node)) {
65 this.showDetails = false;
66 this.requestUpdate();
67 }
Philip Zeyliger6dc90c02025-07-03 20:12:49 -070068 // Close the ports popup when clicking outside of it
69 if (this.showPortsPopup && !this.contains(event.target as Node)) {
70 this.showPortsPopup = false;
71 this.requestUpdate();
72 }
Philip Zeyligere66db3e2025-04-27 15:40:39 +000073 });
74 }
75
76 /**
77 * Toggle the display of detailed information
78 */
79 private _toggleInfoDetails(event: Event) {
80 event.stopPropagation();
81 this.showDetails = !this.showDetails;
82 this.requestUpdate();
Sean McCullough86b56862025-04-18 13:04:03 -070083 }
84
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000085 /**
86 * Update the last commit information based on messages
87 */
88 public updateLastCommitInfo(newMessages: AgentMessage[]): void {
89 if (!newMessages || newMessages.length === 0) return;
90
91 // Process messages in chronological order (latest last)
92 for (const message of newMessages) {
93 if (
94 message.type === "commit" &&
95 message.commits &&
96 message.commits.length > 0
97 ) {
98 // Get the first commit from the list
99 const commit = message.commits[0];
100 if (commit) {
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700101 // Check if the commit hash has changed
102 const hasChanged =
103 !this.lastCommit || this.lastCommit.hash !== commit.hash;
104
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000105 this.lastCommit = {
106 hash: commit.hash,
107 pushedBranch: commit.pushed_branch,
108 };
109 this.lastCommitCopied = false;
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700110
111 // Add pulse animation if the commit changed
112 if (hasChanged) {
113 // Find the last commit element
114 setTimeout(() => {
Sean McCullough7e36a042025-06-25 08:45:18 +0000115 const lastCommitEl = this.querySelector(".last-commit-main");
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700116 if (lastCommitEl) {
117 // Add the pulse class
Sean McCullough7e36a042025-06-25 08:45:18 +0000118 lastCommitEl.classList.add("pulse-custom");
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700119
120 // Remove the pulse class after animation completes
121 setTimeout(() => {
Sean McCullough7e36a042025-06-25 08:45:18 +0000122 lastCommitEl.classList.remove("pulse-custom");
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700123 }, 1500);
124 }
125 }, 0);
126 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000127 }
128 }
129 }
130 }
131
132 /**
133 * Copy commit info to clipboard when clicked
134 */
135 private copyCommitInfo(event: MouseEvent): void {
136 event.preventDefault();
137 event.stopPropagation();
138
139 if (!this.lastCommit) return;
140
141 const textToCopy =
142 this.lastCommit.pushedBranch || this.lastCommit.hash.substring(0, 8);
143
144 navigator.clipboard
145 .writeText(textToCopy)
146 .then(() => {
147 this.lastCommitCopied = true;
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700148 // Reset the copied state after 1.5 seconds
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000149 setTimeout(() => {
150 this.lastCommitCopied = false;
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700151 }, 1500);
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000152 })
153 .catch((err) => {
154 console.error("Failed to copy commit info:", err);
155 });
156 }
157
Philip Zeyligerd1402952025-04-23 03:54:37 +0000158 formatHostname() {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000159 // Only display outside hostname
Philip Zeyliger18532b22025-04-23 21:11:46 +0000160 const outsideHostname = this.state?.outside_hostname;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000161
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000162 if (!outsideHostname) {
Philip Zeyligerd1402952025-04-23 03:54:37 +0000163 return this.state?.hostname;
164 }
165
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000166 return outsideHostname;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000167 }
168
169 formatWorkingDir() {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000170 // Only display outside working directory
Philip Zeyliger18532b22025-04-23 21:11:46 +0000171 const outsideWorkingDir = this.state?.outside_working_dir;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000172
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000173 if (!outsideWorkingDir) {
Philip Zeyligerd1402952025-04-23 03:54:37 +0000174 return this.state?.working_dir;
175 }
176
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000177 return outsideWorkingDir;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000178 }
179
180 getHostnameTooltip() {
Philip Zeyliger18532b22025-04-23 21:11:46 +0000181 const outsideHostname = this.state?.outside_hostname;
182 const insideHostname = this.state?.inside_hostname;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000183
184 if (
Philip Zeyliger18532b22025-04-23 21:11:46 +0000185 !outsideHostname ||
186 !insideHostname ||
187 outsideHostname === insideHostname
Philip Zeyligerd1402952025-04-23 03:54:37 +0000188 ) {
189 return "";
190 }
191
Philip Zeyliger18532b22025-04-23 21:11:46 +0000192 return `Outside: ${outsideHostname}, Inside: ${insideHostname}`;
193 }
194
195 getWorkingDirTooltip() {
196 const outsideWorkingDir = this.state?.outside_working_dir;
197 const insideWorkingDir = this.state?.inside_working_dir;
198
199 if (
200 !outsideWorkingDir ||
201 !insideWorkingDir ||
202 outsideWorkingDir === insideWorkingDir
203 ) {
204 return "";
205 }
206
207 return `Outside: ${outsideWorkingDir}, Inside: ${insideWorkingDir}`;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000208 }
209
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000210 copyToClipboard(text: string) {
211 navigator.clipboard
212 .writeText(text)
213 .then(() => {
214 // Could add a temporary success indicator here
215 })
216 .catch((err) => {
217 console.error("Could not copy text: ", err);
218 });
219 }
220
221 getSSHHostname() {
philip.zeyliger8773e682025-06-11 21:36:21 -0700222 // Use the ssh_connection_string from the state if available, otherwise fall back to generating it
223 return (
224 this.state?.ssh_connection_string || `sketch-${this.state?.session_id}`
225 );
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000226 }
227
philip.zeyligere8da7af2025-06-12 14:24:28 -0700228 getSSHConnectionString() {
229 // Return the connection string for VS Code remote SSH
230 const connectionString =
231 this.state?.ssh_connection_string || `sketch-${this.state?.session_id}`;
232 // If the connection string already contains user@, use it as-is
233 // Otherwise prepend root@ for VS Code remote SSH
234 if (connectionString.includes("@")) {
235 return connectionString;
236 } else {
237 return `root@${connectionString}`;
238 }
239 }
240
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700241 /**
242 * Get sorted ports (by port number) from state, filtering out ports < 1024
243 */
244 getSortedPorts(): Port[] {
245 if (!this.state?.open_ports) {
246 return [];
247 }
248 return [...this.state.open_ports]
Autoformattere48f2bb2025-07-04 04:15:26 +0000249 .filter((port) => port.port >= 1024)
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700250 .sort((a, b) => a.port - b.port);
251 }
252
253 /**
254 * Generate URL for a port based on skaband_addr or localhost
255 */
256 getPortUrl(port: number): string {
257 if (this.state?.skaband_addr) {
258 // Use skaband proxy pattern: skabandaddr/proxy/<sessionId>/<port>
259 return `${this.state.skaband_addr}/proxy/${this.state.session_id}/${port}`;
260 } else {
261 // Use localhost pattern: http://p{port}.localhost:{sketch_port}
262 // We need to extract the port from the current URL
Autoformattere48f2bb2025-07-04 04:15:26 +0000263 const currentPort = window.location.port || "80";
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700264 return `http://p${port}.localhost:${currentPort}`;
265 }
266 }
267
268 /**
269 * Handle port link clicks
270 *
271 * TODO: Whereas Chrome resolves *.localhost as localhost,
272 * Safari does not. Ideally, if skaband_addr is empty, we
273 * could do a quick "fetch(p${port}.localhost)", and, if it
274 * doesn't work at all, we could show the user a modal explaining
275 * to use /etc/hosts. But, anyway, this would be nice but isn't done.
276 */
277 onPortClick(port: number, event: MouseEvent): void {
278 event.preventDefault();
279 event.stopPropagation();
280 const url = this.getPortUrl(port);
Autoformattere48f2bb2025-07-04 04:15:26 +0000281 window.open(url, "_blank");
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700282 }
283
284 /**
285 * Show more ports popup
286 */
287 private _showMorePorts(event: MouseEvent): void {
288 event.preventDefault();
289 event.stopPropagation();
290 this.showPortsPopup = !this.showPortsPopup;
291 this.requestUpdate();
292 }
293
294 /**
295 * Update port tracking and highlight newly opened ports
296 */
297 public updatePortInfo(newPorts: Port[]): void {
Autoformattere48f2bb2025-07-04 04:15:26 +0000298 const currentPorts = newPorts.filter((port) => port.port >= 1024);
299 const previousPortNumbers = new Set(this.previousPorts.map((p) => p.port));
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700300
301 // Find newly opened ports
Autoformattere48f2bb2025-07-04 04:15:26 +0000302 const newlyOpenedPorts = currentPorts.filter(
303 (port) => !previousPortNumbers.has(port.port),
304 );
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700305
306 if (newlyOpenedPorts.length > 0) {
307 // Add newly opened ports to highlighted set
Autoformattere48f2bb2025-07-04 04:15:26 +0000308 newlyOpenedPorts.forEach((port) => {
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700309 this.highlightedPorts.add(port.port);
310 });
311
312 // Remove highlights after animation completes
313 setTimeout(() => {
Autoformattere48f2bb2025-07-04 04:15:26 +0000314 newlyOpenedPorts.forEach((port) => {
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700315 this.highlightedPorts.delete(port.port);
316 });
317 this.requestUpdate();
318 }, 1500);
319 }
320
321 this.previousPorts = [...currentPorts];
322 this.requestUpdate();
323 }
324
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000325 // Format GitHub repository URL to org/repo format
326 formatGitHubRepo(url) {
327 if (!url) return null;
328
329 // Common GitHub URL patterns
330 const patterns = [
331 // HTTPS URLs
Sean McCulloughc7c2cc12025-06-13 03:21:18 +0000332 /https:\/\/github\.com\/([^/]+)\/([^/\s]+?)(?:\.git)?$/,
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000333 // SSH URLs
Sean McCulloughc7c2cc12025-06-13 03:21:18 +0000334 /git@github\.com:([^/]+)\/([^/\s]+?)(?:\.git)?$/,
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000335 // Git protocol
Sean McCulloughc7c2cc12025-06-13 03:21:18 +0000336 /git:\/\/github\.com\/([^/]+)\/([^/\s]+?)(?:\.git)?$/,
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000337 ];
338
339 for (const pattern of patterns) {
340 const match = url.match(pattern);
341 if (match) {
342 return {
343 formatted: `${match[1]}/${match[2]}`,
344 url: `https://github.com/${match[1]}/${match[2]}`,
philip.zeyliger6d3de482025-06-10 19:38:14 -0700345 owner: match[1],
346 repo: match[2],
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000347 };
348 }
349 }
350
351 return null;
352 }
353
philip.zeyliger6d3de482025-06-10 19:38:14 -0700354 // Generate GitHub branch URL if linking is enabled
355 getGitHubBranchLink(branchName) {
356 if (!this.state?.link_to_github || !branchName) {
357 return null;
358 }
359
360 const github = this.formatGitHubRepo(this.state?.git_origin);
361 if (!github) {
362 return null;
363 }
364
365 return `https://github.com/${github.owner}/${github.repo}/tree/${branchName}`;
366 }
367
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000368 renderSSHSection() {
369 // Only show SSH section if we're in a Docker container and have session ID
370 if (!this.state?.session_id) {
371 return html``;
372 }
373
philip.zeyliger26bc6592025-06-30 20:15:30 -0700374 const _sshHost = this.getSSHHostname();
philip.zeyligere8da7af2025-06-12 14:24:28 -0700375 const sshConnectionString = this.getSSHConnectionString();
376 const sshCommand = `ssh ${sshConnectionString}`;
377 const vscodeCommand = `code --remote ssh-remote+${sshConnectionString} /app -n`;
378 const vscodeURL = `vscode://vscode-remote/ssh-remote+${sshConnectionString}/app?windowId=_blank`;
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000379
380 if (!this.state?.ssh_available) {
381 return html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000382 <div class="mt-2.5 pt-2.5 border-t border-gray-300">
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000383 <h3>Connect to Container</h3>
Sean McCullough7e36a042025-06-25 08:45:18 +0000384 <div
385 class="bg-orange-50 border-l-4 border-orange-500 p-3 mt-2 text-xs text-orange-800"
386 >
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000387 SSH connections are not available:
388 ${this.state?.ssh_error || "SSH configuration is missing"}
389 </div>
390 </div>
391 `;
392 }
393
394 return html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000395 <div class="mt-2.5 pt-2.5 border-t border-gray-300">
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000396 <h3>Connect to Container</h3>
Sean McCullough7e36a042025-06-25 08:45:18 +0000397 <div class="flex items-center mb-2 gap-2.5">
398 <div
399 class="font-mono text-xs bg-gray-100 px-2 py-1 rounded border border-gray-300 flex-grow"
400 >
401 ${sshCommand}
402 </div>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000403 <button
Sean McCullough7e36a042025-06-25 08:45:18 +0000404 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 +0000405 @click=${() => this.copyToClipboard(sshCommand)}
406 >
407 Copy
408 </button>
409 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000410 <div class="flex items-center mb-2 gap-2.5">
411 <div
412 class="font-mono text-xs bg-gray-100 px-2 py-1 rounded border border-gray-300 flex-grow"
413 >
414 ${vscodeCommand}
415 </div>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000416 <button
Sean McCullough7e36a042025-06-25 08:45:18 +0000417 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 +0000418 @click=${() => this.copyToClipboard(vscodeCommand)}
419 >
420 Copy
421 </button>
422 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000423 <div class="flex items-center mb-2 gap-2.5">
424 <a
425 href="${vscodeURL}"
426 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"
427 title="${vscodeURL}"
428 >
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000429 <svg
Sean McCullough7e36a042025-06-25 08:45:18 +0000430 class="w-4 h-4"
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000431 xmlns="http://www.w3.org/2000/svg"
432 viewBox="0 0 24 24"
433 fill="none"
434 stroke="white"
435 stroke-width="2"
436 stroke-linecap="round"
437 stroke-linejoin="round"
438 >
439 <path
440 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"
441 />
442 </svg>
443 <span>Open in VSCode</span>
444 </a>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000445 </div>
446 </div>
447 `;
448 }
449
Sean McCullough86b56862025-04-18 13:04:03 -0700450 render() {
451 return html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000452 <div class="flex items-center relative">
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700453 <!-- Main visible info in two columns - github/hostname/dir and last commit -->
Sean McCullough7e36a042025-06-25 08:45:18 +0000454 <div class="flex flex-wrap gap-2 px-2.5 py-1 flex-1">
Sean McCullough49577492025-06-26 17:13:28 -0700455 <div class="flex gap-2.5 w-full">
Sean McCullough7e36a042025-06-25 08:45:18 +0000456 <!-- First column: GitHub repo (or hostname) and working dir -->
457 <div class="flex flex-col gap-0.5">
458 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
459 ${(() => {
460 const github = this.formatGitHubRepo(this.state?.git_origin);
461 if (github) {
462 return html`
463 <a
464 href="${github.url}"
465 target="_blank"
466 rel="noopener noreferrer"
467 class="github-link text-blue-600 no-underline hover:underline"
468 title="${this.state?.git_origin}"
469 >
470 ${github.formatted}
471 </a>
472 `;
473 } else {
474 return html`
475 <span
476 id="hostname"
477 class="text-xs font-semibold break-all cursor-default"
478 title="${this.getHostnameTooltip()}"
479 >
480 ${this.formatHostname()}
481 </span>
482 `;
483 }
484 })()}
485 </div>
486 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
487 <span
488 id="workingDir"
489 class="text-xs font-semibold break-all cursor-default"
490 title="${this.getWorkingDirTooltip()}"
491 >
492 ${this.formatWorkingDir()}
493 </span>
494 </div>
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000495 </div>
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000496
Sean McCullough7e36a042025-06-25 08:45:18 +0000497 <!-- Second column: Last Commit -->
498 <div class="flex flex-col gap-0.5 justify-start">
499 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
500 <span class="text-xs text-gray-600 font-medium"
501 >Last Commit</span
502 >
503 </div>
504 <div
505 class="flex items-center whitespace-nowrap mr-2.5 text-xs cursor-pointer relative pt-0 last-commit-main hover:text-blue-600"
506 @click=${(e: MouseEvent) => this.copyCommitInfo(e)}
507 title="Click to copy"
508 >
509 ${this.lastCommit
510 ? this.lastCommit.pushedBranch
511 ? (() => {
512 const githubLink = this.getGitHubBranchLink(
513 this.lastCommit.pushedBranch,
514 );
515 return html`
516 <div class="flex items-center gap-1.5">
517 <span
518 class="text-green-600 font-mono text-xs whitespace-nowrap overflow-hidden text-ellipsis"
519 title="Click to copy: ${this.lastCommit
520 .pushedBranch}"
521 @click=${(e) => this.copyCommitInfo(e)}
522 >${this.lastCommit.pushedBranch}</span
523 >
524 <span
525 class="ml-1 opacity-70 flex items-center hover:opacity-100"
526 >
527 ${this.lastCommitCopied
528 ? html`<svg
529 xmlns="http://www.w3.org/2000/svg"
530 width="16"
531 height="16"
532 viewBox="0 0 24 24"
533 fill="none"
534 stroke="currentColor"
535 stroke-width="2"
536 stroke-linecap="round"
537 stroke-linejoin="round"
538 class="align-middle"
539 >
540 <path d="M20 6L9 17l-5-5"></path>
541 </svg>`
542 : html`<svg
543 xmlns="http://www.w3.org/2000/svg"
544 width="16"
545 height="16"
546 viewBox="0 0 24 24"
547 fill="none"
548 stroke="currentColor"
549 stroke-width="2"
550 stroke-linecap="round"
551 stroke-linejoin="round"
552 class="align-middle"
553 >
554 <rect
555 x="9"
556 y="9"
557 width="13"
558 height="13"
559 rx="2"
560 ry="2"
561 ></rect>
562 <path
563 d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
564 ></path>
565 </svg>`}
566 </span>
567 ${githubLink
568 ? html`<a
569 href="${githubLink}"
570 target="_blank"
571 rel="noopener noreferrer"
572 class="text-gray-600 no-underline flex items-center transition-colors hover:text-blue-600"
573 title="Open ${this.lastCommit
574 .pushedBranch} on GitHub"
575 @click=${(e) => e.stopPropagation()}
philip.zeyliger6d3de482025-06-10 19:38:14 -0700576 >
Sean McCullough7e36a042025-06-25 08:45:18 +0000577 <svg
578 class="w-4 h-4"
579 viewBox="0 0 16 16"
580 width="16"
581 height="16"
582 >
583 <path
584 fill="currentColor"
585 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"
586 />
587 </svg>
588 </a>`
589 : ""}
590 </div>
591 `;
592 })()
593 : html`<span
594 class="text-gray-600 font-mono text-xs whitespace-nowrap overflow-hidden text-ellipsis"
595 >${this.lastCommit.hash.substring(0, 8)}</span
596 >`
597 : html`<span class="text-gray-500 italic text-xs">N/A</span>`}
598 </div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000599 </div>
600 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700601 </div>
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000602
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700603 <!-- Ports section -->
604 ${(() => {
605 const ports = this.getSortedPorts();
606 if (ports.length === 0) {
607 return html``;
608 }
609 const displayPorts = ports.slice(0, 2);
610 const remainingPorts = ports.slice(2);
611 return html`
612 <div class="flex items-center gap-1 ml-2">
Autoformattere48f2bb2025-07-04 04:15:26 +0000613 ${displayPorts.map(
614 (port) => html`
615 <button
616 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(
617 port.port,
618 )
619 ? "pulse-custom"
620 : ""}"
621 @click=${(e: MouseEvent) => this.onPortClick(port.port, e)}
622 title="Open ${port.process} on port ${port.port}"
623 >
624 <span>${port.process}(${port.port})</span>
625 <span>🔗</span>
626 </button>
627 `,
628 )}
629 ${remainingPorts.length > 0
630 ? html`
631 <button
632 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(
633 (port) => this.highlightedPorts.has(port.port),
634 )
635 ? "pulse-custom"
636 : ""}"
637 @click=${(e: MouseEvent) => this._showMorePorts(e)}
638 title="Show ${remainingPorts.length} more ports"
639 >
640 +${remainingPorts.length}
641 </button>
642 `
643 : html``}
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700644 </div>
645 `;
646 })()}
647
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000648 <!-- Info toggle button -->
649 <button
Sean McCullough7e36a042025-06-25 08:45:18 +0000650 class="info-toggle ml-2 w-6 h-6 rounded-full flex items-center justify-center ${this
651 .showDetails
652 ? "bg-blue-500 text-white border-blue-600"
653 : "bg-gray-100 text-gray-600 border-gray-300"} border cursor-pointer font-bold italic transition-all hover:${this
654 .showDetails
655 ? "bg-blue-600"
656 : "bg-gray-200"}"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000657 @click=${this._toggleInfoDetails}
658 title="Show/hide details"
659 >
660 i
661 </button>
662
663 <!-- Expanded info panel -->
Sean McCullough7e36a042025-06-25 08:45:18 +0000664 <div
665 class="${this.showDetails
666 ? "block"
Sean McCulloughc52a2502025-07-06 20:27:59 -0700667 : "hidden"} absolute min-w-max top-full z-100 bg-white rounded-lg p-4 shadow-lg mt-1.5"
Sean McCullough7e36a042025-06-25 08:45:18 +0000668 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000669 <!-- Last Commit section moved to main grid -->
670
Sean McCullough7e36a042025-06-25 08:45:18 +0000671 <div
Sean McCulloughc37e0662025-07-03 08:46:21 -0700672 class="grid gap-2 mt-2.5"
673 style="grid-template-columns: auto auto"
Sean McCullough7e36a042025-06-25 08:45:18 +0000674 >
675 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
676 <span class="text-xs text-gray-600 mr-1 font-medium"
677 >Commit:</span
678 >
679 <span id="initialCommit" class="text-xs font-semibold break-all"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000680 >${this.state?.initial_commit?.substring(0, 8)}</span
681 >
682 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000683 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
684 <span class="text-xs text-gray-600 mr-1 font-medium">Msgs:</span>
685 <span id="messageCount" class="text-xs font-semibold break-all"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000686 >${this.state?.message_count}</span
687 >
688 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000689 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
690 <span class="text-xs text-gray-600 mr-1 font-medium"
691 >Session ID:</span
692 >
693 <span id="sessionId" class="text-xs font-semibold break-all"
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000694 >${this.state?.session_id || "N/A"}</span
695 >
696 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000697 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
698 <span class="text-xs text-gray-600 mr-1 font-medium"
699 >Hostname:</span
700 >
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700701 <span
702 id="hostnameDetail"
Sean McCullough7e36a042025-06-25 08:45:18 +0000703 class="text-xs font-semibold break-all cursor-default"
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700704 title="${this.getHostnameTooltip()}"
705 >
706 ${this.formatHostname()}
707 </span>
708 </div>
Philip Zeyliger72318392025-05-14 02:56:07 +0000709 ${this.state?.agent_state
710 ? html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000711 <div
712 class="flex items-center whitespace-nowrap mr-2.5 text-xs"
713 >
714 <span class="text-xs text-gray-600 mr-1 font-medium"
715 >Agent State:</span
716 >
717 <span
718 id="agentState"
719 class="text-xs font-semibold break-all"
Philip Zeyliger72318392025-05-14 02:56:07 +0000720 >${this.state?.agent_state}</span
721 >
722 </div>
723 `
724 : ""}
Sean McCullough7e36a042025-06-25 08:45:18 +0000725 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
726 <span class="text-xs text-gray-600 mr-1 font-medium"
727 >Input tokens:</span
728 >
729 <span id="inputTokens" class="text-xs font-semibold break-all"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000730 >${formatNumber(
731 (this.state?.total_usage?.input_tokens || 0) +
732 (this.state?.total_usage?.cache_read_input_tokens || 0) +
733 (this.state?.total_usage?.cache_creation_input_tokens || 0),
734 )}</span
735 >
736 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000737 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
738 <span class="text-xs text-gray-600 mr-1 font-medium"
Sean McCulloughc37e0662025-07-03 08:46:21 -0700739 >Context size:</span
740 >
741 <span id="contextWindow" class="text-xs font-semibold break-all"
742 >${formatNumber(
743 (this.latestUsage?.input_tokens || 0) +
744 (this.latestUsage?.cache_read_input_tokens || 0) +
745 (this.latestUsage?.cache_creation_input_tokens || 0),
746 )}</span
747 >
748 </div>
749 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
750 <span class="text-xs text-gray-600 mr-1 font-medium"
Sean McCullough7e36a042025-06-25 08:45:18 +0000751 >Output tokens:</span
752 >
753 <span id="outputTokens" class="text-xs font-semibold break-all"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000754 >${formatNumber(this.state?.total_usage?.output_tokens)}</span
755 >
756 </div>
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700757 ${(this.state?.total_usage?.total_cost_usd || 0) > 0
758 ? html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000759 <div
760 class="flex items-center whitespace-nowrap mr-2.5 text-xs"
761 >
762 <span class="text-xs text-gray-600 mr-1 font-medium"
763 >Total cost:</span
764 >
765 <span id="totalCost" class="text-xs font-semibold break-all"
philip.zeyliger26bc6592025-06-30 20:15:30 -0700766 >$${(
767 this.state?.total_usage?.total_cost_usd ?? 0
768 ).toFixed(2)}</span
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700769 >
770 </div>
771 `
772 : ""}
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000773 <div
Sean McCullough7e36a042025-06-25 08:45:18 +0000774 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 +0000775 >
Sean McCullough7e36a042025-06-25 08:45:18 +0000776 <a href="logs" class="text-blue-600">Logs</a> (<a
777 href="download"
778 class="text-blue-600"
779 >Download</a
780 >)
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000781 </div>
782 </div>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000783
784 <!-- SSH Connection Information -->
785 ${this.renderSSHSection()}
Sean McCullough86b56862025-04-18 13:04:03 -0700786 </div>
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700787
788 <!-- Ports popup -->
789 <div
790 class="${this.showPortsPopup
791 ? "block"
792 : "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"
793 >
794 <h3 class="text-sm font-semibold mb-2">Open Ports</h3>
795 <div class="flex flex-col gap-1">
Autoformattere48f2bb2025-07-04 04:15:26 +0000796 ${this.getSortedPorts().map(
797 (port) => html`
798 <button
799 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"
800 @click=${(e: MouseEvent) => this.onPortClick(port.port, e)}
801 title="Open ${port.process} on port ${port.port}"
802 >
803 <span>${port.process}(${port.port})</span>
804 <span>🔗</span>
805 </button>
806 `,
807 )}
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700808 </div>
809 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700810 </div>
811 `;
812 }
813}
814
815declare global {
816 interface HTMLElementTagNameMap {
817 "sketch-container-status": SketchContainerStatus;
818 }
819}