blob: 511c38f27279b01291df879335e287dcfaceecc2 [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]
249 .filter(port => port.port >= 1024)
250 .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
263 const currentPort = window.location.port || '80';
264 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);
281 window.open(url, '_blank');
282 }
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 {
298 const currentPorts = newPorts.filter(port => port.port >= 1024);
299 const previousPortNumbers = new Set(this.previousPorts.map(p => p.port));
300
301 // Find newly opened ports
302 const newlyOpenedPorts = currentPorts.filter(port => !previousPortNumbers.has(port.port));
303
304 if (newlyOpenedPorts.length > 0) {
305 // Add newly opened ports to highlighted set
306 newlyOpenedPorts.forEach(port => {
307 this.highlightedPorts.add(port.port);
308 });
309
310 // Remove highlights after animation completes
311 setTimeout(() => {
312 newlyOpenedPorts.forEach(port => {
313 this.highlightedPorts.delete(port.port);
314 });
315 this.requestUpdate();
316 }, 1500);
317 }
318
319 this.previousPorts = [...currentPorts];
320 this.requestUpdate();
321 }
322
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000323 // Format GitHub repository URL to org/repo format
324 formatGitHubRepo(url) {
325 if (!url) return null;
326
327 // Common GitHub URL patterns
328 const patterns = [
329 // HTTPS URLs
Sean McCulloughc7c2cc12025-06-13 03:21:18 +0000330 /https:\/\/github\.com\/([^/]+)\/([^/\s]+?)(?:\.git)?$/,
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000331 // SSH URLs
Sean McCulloughc7c2cc12025-06-13 03:21:18 +0000332 /git@github\.com:([^/]+)\/([^/\s]+?)(?:\.git)?$/,
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000333 // Git protocol
Sean McCulloughc7c2cc12025-06-13 03:21:18 +0000334 /git:\/\/github\.com\/([^/]+)\/([^/\s]+?)(?:\.git)?$/,
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000335 ];
336
337 for (const pattern of patterns) {
338 const match = url.match(pattern);
339 if (match) {
340 return {
341 formatted: `${match[1]}/${match[2]}`,
342 url: `https://github.com/${match[1]}/${match[2]}`,
philip.zeyliger6d3de482025-06-10 19:38:14 -0700343 owner: match[1],
344 repo: match[2],
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000345 };
346 }
347 }
348
349 return null;
350 }
351
philip.zeyliger6d3de482025-06-10 19:38:14 -0700352 // Generate GitHub branch URL if linking is enabled
353 getGitHubBranchLink(branchName) {
354 if (!this.state?.link_to_github || !branchName) {
355 return null;
356 }
357
358 const github = this.formatGitHubRepo(this.state?.git_origin);
359 if (!github) {
360 return null;
361 }
362
363 return `https://github.com/${github.owner}/${github.repo}/tree/${branchName}`;
364 }
365
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000366 renderSSHSection() {
367 // Only show SSH section if we're in a Docker container and have session ID
368 if (!this.state?.session_id) {
369 return html``;
370 }
371
philip.zeyliger26bc6592025-06-30 20:15:30 -0700372 const _sshHost = this.getSSHHostname();
philip.zeyligere8da7af2025-06-12 14:24:28 -0700373 const sshConnectionString = this.getSSHConnectionString();
374 const sshCommand = `ssh ${sshConnectionString}`;
375 const vscodeCommand = `code --remote ssh-remote+${sshConnectionString} /app -n`;
376 const vscodeURL = `vscode://vscode-remote/ssh-remote+${sshConnectionString}/app?windowId=_blank`;
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000377
378 if (!this.state?.ssh_available) {
379 return html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000380 <div class="mt-2.5 pt-2.5 border-t border-gray-300">
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000381 <h3>Connect to Container</h3>
Sean McCullough7e36a042025-06-25 08:45:18 +0000382 <div
383 class="bg-orange-50 border-l-4 border-orange-500 p-3 mt-2 text-xs text-orange-800"
384 >
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000385 SSH connections are not available:
386 ${this.state?.ssh_error || "SSH configuration is missing"}
387 </div>
388 </div>
389 `;
390 }
391
392 return html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000393 <div class="mt-2.5 pt-2.5 border-t border-gray-300">
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000394 <h3>Connect to Container</h3>
Sean McCullough7e36a042025-06-25 08:45:18 +0000395 <div class="flex items-center mb-2 gap-2.5">
396 <div
397 class="font-mono text-xs bg-gray-100 px-2 py-1 rounded border border-gray-300 flex-grow"
398 >
399 ${sshCommand}
400 </div>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000401 <button
Sean McCullough7e36a042025-06-25 08:45:18 +0000402 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 +0000403 @click=${() => this.copyToClipboard(sshCommand)}
404 >
405 Copy
406 </button>
407 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000408 <div class="flex items-center mb-2 gap-2.5">
409 <div
410 class="font-mono text-xs bg-gray-100 px-2 py-1 rounded border border-gray-300 flex-grow"
411 >
412 ${vscodeCommand}
413 </div>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000414 <button
Sean McCullough7e36a042025-06-25 08:45:18 +0000415 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 +0000416 @click=${() => this.copyToClipboard(vscodeCommand)}
417 >
418 Copy
419 </button>
420 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000421 <div class="flex items-center mb-2 gap-2.5">
422 <a
423 href="${vscodeURL}"
424 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"
425 title="${vscodeURL}"
426 >
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000427 <svg
Sean McCullough7e36a042025-06-25 08:45:18 +0000428 class="w-4 h-4"
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000429 xmlns="http://www.w3.org/2000/svg"
430 viewBox="0 0 24 24"
431 fill="none"
432 stroke="white"
433 stroke-width="2"
434 stroke-linecap="round"
435 stroke-linejoin="round"
436 >
437 <path
438 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"
439 />
440 </svg>
441 <span>Open in VSCode</span>
442 </a>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000443 </div>
444 </div>
445 `;
446 }
447
Sean McCullough86b56862025-04-18 13:04:03 -0700448 render() {
449 return html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000450 <div class="flex items-center relative">
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700451 <!-- Main visible info in two columns - github/hostname/dir and last commit -->
Sean McCullough7e36a042025-06-25 08:45:18 +0000452 <div class="flex flex-wrap gap-2 px-2.5 py-1 flex-1">
Sean McCullough49577492025-06-26 17:13:28 -0700453 <div class="flex gap-2.5 w-full">
Sean McCullough7e36a042025-06-25 08:45:18 +0000454 <!-- First column: GitHub repo (or hostname) and working dir -->
455 <div class="flex flex-col gap-0.5">
456 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
457 ${(() => {
458 const github = this.formatGitHubRepo(this.state?.git_origin);
459 if (github) {
460 return html`
461 <a
462 href="${github.url}"
463 target="_blank"
464 rel="noopener noreferrer"
465 class="github-link text-blue-600 no-underline hover:underline"
466 title="${this.state?.git_origin}"
467 >
468 ${github.formatted}
469 </a>
470 `;
471 } else {
472 return html`
473 <span
474 id="hostname"
475 class="text-xs font-semibold break-all cursor-default"
476 title="${this.getHostnameTooltip()}"
477 >
478 ${this.formatHostname()}
479 </span>
480 `;
481 }
482 })()}
483 </div>
484 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
485 <span
486 id="workingDir"
487 class="text-xs font-semibold break-all cursor-default"
488 title="${this.getWorkingDirTooltip()}"
489 >
490 ${this.formatWorkingDir()}
491 </span>
492 </div>
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000493 </div>
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000494
Sean McCullough7e36a042025-06-25 08:45:18 +0000495 <!-- Second column: Last Commit -->
496 <div class="flex flex-col gap-0.5 justify-start">
497 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
498 <span class="text-xs text-gray-600 font-medium"
499 >Last Commit</span
500 >
501 </div>
502 <div
503 class="flex items-center whitespace-nowrap mr-2.5 text-xs cursor-pointer relative pt-0 last-commit-main hover:text-blue-600"
504 @click=${(e: MouseEvent) => this.copyCommitInfo(e)}
505 title="Click to copy"
506 >
507 ${this.lastCommit
508 ? this.lastCommit.pushedBranch
509 ? (() => {
510 const githubLink = this.getGitHubBranchLink(
511 this.lastCommit.pushedBranch,
512 );
513 return html`
514 <div class="flex items-center gap-1.5">
515 <span
516 class="text-green-600 font-mono text-xs whitespace-nowrap overflow-hidden text-ellipsis"
517 title="Click to copy: ${this.lastCommit
518 .pushedBranch}"
519 @click=${(e) => this.copyCommitInfo(e)}
520 >${this.lastCommit.pushedBranch}</span
521 >
522 <span
523 class="ml-1 opacity-70 flex items-center hover:opacity-100"
524 >
525 ${this.lastCommitCopied
526 ? html`<svg
527 xmlns="http://www.w3.org/2000/svg"
528 width="16"
529 height="16"
530 viewBox="0 0 24 24"
531 fill="none"
532 stroke="currentColor"
533 stroke-width="2"
534 stroke-linecap="round"
535 stroke-linejoin="round"
536 class="align-middle"
537 >
538 <path d="M20 6L9 17l-5-5"></path>
539 </svg>`
540 : html`<svg
541 xmlns="http://www.w3.org/2000/svg"
542 width="16"
543 height="16"
544 viewBox="0 0 24 24"
545 fill="none"
546 stroke="currentColor"
547 stroke-width="2"
548 stroke-linecap="round"
549 stroke-linejoin="round"
550 class="align-middle"
551 >
552 <rect
553 x="9"
554 y="9"
555 width="13"
556 height="13"
557 rx="2"
558 ry="2"
559 ></rect>
560 <path
561 d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
562 ></path>
563 </svg>`}
564 </span>
565 ${githubLink
566 ? html`<a
567 href="${githubLink}"
568 target="_blank"
569 rel="noopener noreferrer"
570 class="text-gray-600 no-underline flex items-center transition-colors hover:text-blue-600"
571 title="Open ${this.lastCommit
572 .pushedBranch} on GitHub"
573 @click=${(e) => e.stopPropagation()}
philip.zeyliger6d3de482025-06-10 19:38:14 -0700574 >
Sean McCullough7e36a042025-06-25 08:45:18 +0000575 <svg
576 class="w-4 h-4"
577 viewBox="0 0 16 16"
578 width="16"
579 height="16"
580 >
581 <path
582 fill="currentColor"
583 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"
584 />
585 </svg>
586 </a>`
587 : ""}
588 </div>
589 `;
590 })()
591 : html`<span
592 class="text-gray-600 font-mono text-xs whitespace-nowrap overflow-hidden text-ellipsis"
593 >${this.lastCommit.hash.substring(0, 8)}</span
594 >`
595 : html`<span class="text-gray-500 italic text-xs">N/A</span>`}
596 </div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000597 </div>
598 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700599 </div>
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000600
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700601 <!-- Ports section -->
602 ${(() => {
603 const ports = this.getSortedPorts();
604 if (ports.length === 0) {
605 return html``;
606 }
607 const displayPorts = ports.slice(0, 2);
608 const remainingPorts = ports.slice(2);
609 return html`
610 <div class="flex items-center gap-1 ml-2">
611 ${displayPorts.map(port => html`
612 <button
613 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(port.port) ? 'pulse-custom' : ''}"
614 @click=${(e: MouseEvent) => this.onPortClick(port.port, e)}
615 title="Open ${port.process} on port ${port.port}"
616 >
617 <span>${port.process}(${port.port})</span>
618 <span>🔗</span>
619 </button>
620 `)}
621 ${remainingPorts.length > 0 ? html`
622 <button
623 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(port => this.highlightedPorts.has(port.port)) ? 'pulse-custom' : ''}"
624 @click=${(e: MouseEvent) => this._showMorePorts(e)}
625 title="Show ${remainingPorts.length} more ports"
626 >
627 +${remainingPorts.length}
628 </button>
629 ` : html``}
630 </div>
631 `;
632 })()}
633
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000634 <!-- Info toggle button -->
635 <button
Sean McCullough7e36a042025-06-25 08:45:18 +0000636 class="info-toggle ml-2 w-6 h-6 rounded-full flex items-center justify-center ${this
637 .showDetails
638 ? "bg-blue-500 text-white border-blue-600"
639 : "bg-gray-100 text-gray-600 border-gray-300"} border cursor-pointer font-bold italic transition-all hover:${this
640 .showDetails
641 ? "bg-blue-600"
642 : "bg-gray-200"}"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000643 @click=${this._toggleInfoDetails}
644 title="Show/hide details"
645 >
646 i
647 </button>
648
649 <!-- Expanded info panel -->
Sean McCullough7e36a042025-06-25 08:45:18 +0000650 <div
651 class="${this.showDetails
652 ? "block"
653 : "hidden"} absolute min-w-max top-full z-10 bg-white rounded-lg p-4 shadow-lg mt-1.5"
654 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000655 <!-- Last Commit section moved to main grid -->
656
Sean McCullough7e36a042025-06-25 08:45:18 +0000657 <div
Sean McCulloughc37e0662025-07-03 08:46:21 -0700658 class="grid gap-2 mt-2.5"
659 style="grid-template-columns: auto auto"
Sean McCullough7e36a042025-06-25 08:45:18 +0000660 >
661 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
662 <span class="text-xs text-gray-600 mr-1 font-medium"
663 >Commit:</span
664 >
665 <span id="initialCommit" class="text-xs font-semibold break-all"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000666 >${this.state?.initial_commit?.substring(0, 8)}</span
667 >
668 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000669 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
670 <span class="text-xs text-gray-600 mr-1 font-medium">Msgs:</span>
671 <span id="messageCount" class="text-xs font-semibold break-all"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000672 >${this.state?.message_count}</span
673 >
674 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000675 <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 >Session ID:</span
678 >
679 <span id="sessionId" class="text-xs font-semibold break-all"
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000680 >${this.state?.session_id || "N/A"}</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"
685 >Hostname:</span
686 >
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700687 <span
688 id="hostnameDetail"
Sean McCullough7e36a042025-06-25 08:45:18 +0000689 class="text-xs font-semibold break-all cursor-default"
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700690 title="${this.getHostnameTooltip()}"
691 >
692 ${this.formatHostname()}
693 </span>
694 </div>
Philip Zeyliger72318392025-05-14 02:56:07 +0000695 ${this.state?.agent_state
696 ? html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000697 <div
698 class="flex items-center whitespace-nowrap mr-2.5 text-xs"
699 >
700 <span class="text-xs text-gray-600 mr-1 font-medium"
701 >Agent State:</span
702 >
703 <span
704 id="agentState"
705 class="text-xs font-semibold break-all"
Philip Zeyliger72318392025-05-14 02:56:07 +0000706 >${this.state?.agent_state}</span
707 >
708 </div>
709 `
710 : ""}
Sean McCullough7e36a042025-06-25 08:45:18 +0000711 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
712 <span class="text-xs text-gray-600 mr-1 font-medium"
713 >Input tokens:</span
714 >
715 <span id="inputTokens" class="text-xs font-semibold break-all"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000716 >${formatNumber(
717 (this.state?.total_usage?.input_tokens || 0) +
718 (this.state?.total_usage?.cache_read_input_tokens || 0) +
719 (this.state?.total_usage?.cache_creation_input_tokens || 0),
720 )}</span
721 >
722 </div>
Sean McCullough7e36a042025-06-25 08:45:18 +0000723 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
724 <span class="text-xs text-gray-600 mr-1 font-medium"
Sean McCulloughc37e0662025-07-03 08:46:21 -0700725 >Context size:</span
726 >
727 <span id="contextWindow" class="text-xs font-semibold break-all"
728 >${formatNumber(
729 (this.latestUsage?.input_tokens || 0) +
730 (this.latestUsage?.cache_read_input_tokens || 0) +
731 (this.latestUsage?.cache_creation_input_tokens || 0),
732 )}</span
733 >
734 </div>
735 <div class="flex items-center whitespace-nowrap mr-2.5 text-xs">
736 <span class="text-xs text-gray-600 mr-1 font-medium"
Sean McCullough7e36a042025-06-25 08:45:18 +0000737 >Output tokens:</span
738 >
739 <span id="outputTokens" class="text-xs font-semibold break-all"
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000740 >${formatNumber(this.state?.total_usage?.output_tokens)}</span
741 >
742 </div>
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700743 ${(this.state?.total_usage?.total_cost_usd || 0) > 0
744 ? html`
Sean McCullough7e36a042025-06-25 08:45:18 +0000745 <div
746 class="flex items-center whitespace-nowrap mr-2.5 text-xs"
747 >
748 <span class="text-xs text-gray-600 mr-1 font-medium"
749 >Total cost:</span
750 >
751 <span id="totalCost" class="text-xs font-semibold break-all"
philip.zeyliger26bc6592025-06-30 20:15:30 -0700752 >$${(
753 this.state?.total_usage?.total_cost_usd ?? 0
754 ).toFixed(2)}</span
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700755 >
756 </div>
757 `
758 : ""}
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000759 <div
Sean McCullough7e36a042025-06-25 08:45:18 +0000760 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 +0000761 >
Sean McCullough7e36a042025-06-25 08:45:18 +0000762 <a href="logs" class="text-blue-600">Logs</a> (<a
763 href="download"
764 class="text-blue-600"
765 >Download</a
766 >)
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000767 </div>
768 </div>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000769
770 <!-- SSH Connection Information -->
771 ${this.renderSSHSection()}
Sean McCullough86b56862025-04-18 13:04:03 -0700772 </div>
Philip Zeyliger6dc90c02025-07-03 20:12:49 -0700773
774 <!-- Ports popup -->
775 <div
776 class="${this.showPortsPopup
777 ? "block"
778 : "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"
779 >
780 <h3 class="text-sm font-semibold mb-2">Open Ports</h3>
781 <div class="flex flex-col gap-1">
782 ${this.getSortedPorts().map(port => html`
783 <button
784 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"
785 @click=${(e: MouseEvent) => this.onPortClick(port.port, e)}
786 title="Open ${port.process} on port ${port.port}"
787 >
788 <span>${port.process}(${port.port})</span>
789 <span>🔗</span>
790 </button>
791 `)}
792 </div>
793 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700794 </div>
795 `;
796 }
797}
798
799declare global {
800 interface HTMLElementTagNameMap {
801 "sketch-container-status": SketchContainerStatus;
802 }
803}