blob: 088244f03f1ce7e0045f9b822e294bd0b9165049 [file] [log] [blame]
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001import { State, AgentMessage } from "../types";
Sean McCulloughb29f8912025-04-20 15:39:11 -07002import { LitElement, css, 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 McCullough86b56862025-04-18 13:04:03 -07005
6@customElement("sketch-container-status")
7export class SketchContainerStatus extends LitElement {
8 // Header bar: Container status details
9
10 @property()
11 state: State;
12
Philip Zeyligere66db3e2025-04-27 15:40:39 +000013 @state()
14 showDetails: boolean = false;
15
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000016 @state()
17 lastCommit: { hash: string; pushedBranch?: string } | null = null;
18
19 @state()
20 lastCommitCopied: boolean = false;
21
Sean McCullough86b56862025-04-18 13:04:03 -070022 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
23 // Note that these styles only apply to the scope of this web component's
24 // shadow DOM node, so they won't leak out or collide with CSS declared in
25 // other components or the containing web page (...unless you want it to do that).
26 static styles = css`
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000027 /* Last commit display styling */
28 .last-commit {
29 display: flex;
30 flex-direction: column;
31 padding: 3px 8px;
32 cursor: pointer;
33 position: relative;
34 margin: 4px 0;
35 transition: color 0.2s ease;
36 }
37
38 .last-commit:hover {
39 color: #0366d6;
40 }
41
Philip Zeyliger9bca61e2025-05-22 12:40:06 -070042 /* Pulse animation for new commits */
43 @keyframes pulse {
44 0% {
45 transform: scale(1);
46 opacity: 1;
47 }
48 50% {
49 transform: scale(1.05);
50 opacity: 0.8;
51 }
52 100% {
53 transform: scale(1);
54 opacity: 1;
55 }
56 }
57
58 .pulse {
59 animation: pulse 1.5s ease-in-out;
60 background-color: rgba(38, 132, 255, 0.1);
61 border-radius: 3px;
62 }
63
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000064 .last-commit-title {
65 color: #666;
66 font-family: system-ui, sans-serif;
67 font-size: 11px;
68 font-weight: 500;
69 line-height: 1.2;
70 }
71
72 .last-commit-hash {
73 font-family: monospace;
74 font-size: 12px;
75 white-space: nowrap;
76 overflow: hidden;
77 text-overflow: ellipsis;
78 }
79
80 /* Styles for the last commit in main grid */
81 .last-commit-column {
82 justify-content: flex-start;
83 }
84
85 .info-label {
86 color: #666;
87 font-family: system-ui, sans-serif;
88 font-size: 11px;
89 font-weight: 500;
90 }
91
92 .last-commit-main {
93 cursor: pointer;
94 position: relative;
95 padding-top: 0;
96 }
97
98 .last-commit-main:hover {
99 color: #0366d6;
100 }
101
102 .main-grid-commit {
103 font-family: monospace;
104 font-size: 12px;
105 white-space: nowrap;
106 overflow: hidden;
107 text-overflow: ellipsis;
108 }
109
110 .commit-hash-indicator {
111 color: #666;
112 }
113
114 .commit-branch-indicator {
115 color: #28a745;
116 }
117
118 .no-commit-indicator {
119 color: #999;
120 font-style: italic;
121 font-size: 12px;
122 }
123
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700124 .copied-indicator {
125 position: absolute;
126 top: 0;
127 left: 0;
128 background: rgba(0, 0, 0, 0.7);
129 color: white;
130 padding: 2px 6px;
131 border-radius: 3px;
132 font-size: 11px;
133 pointer-events: none;
134 z-index: 10;
135 }
136
137 .copy-icon {
138 margin-left: 4px;
139 opacity: 0.7;
140 }
141
142 .copy-icon svg {
143 vertical-align: middle;
144 }
145
146 .last-commit-main:hover .copy-icon {
147 opacity: 1;
148 }
149
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000150 .info-container {
151 display: flex;
152 align-items: center;
153 position: relative;
Sean McCullough86b56862025-04-18 13:04:03 -0700154 }
155
156 .info-grid {
157 display: flex;
158 flex-wrap: wrap;
159 gap: 8px;
160 background: #f9f9f9;
161 border-radius: 4px;
162 padding: 4px 10px;
163 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
164 flex: 1;
165 }
166
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000167 .info-expanded {
168 position: absolute;
169 top: 100%;
170 right: 0;
171 z-index: 10;
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000172 min-width: 400px;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000173 background: white;
174 border-radius: 8px;
175 padding: 10px 15px;
176 box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
177 margin-top: 5px;
178 display: none;
179 }
180
181 .info-expanded.active {
182 display: block;
183 }
184
Sean McCullough86b56862025-04-18 13:04:03 -0700185 .info-item {
186 display: flex;
187 align-items: center;
188 white-space: nowrap;
189 margin-right: 10px;
190 font-size: 13px;
191 }
192
193 .info-label {
194 font-size: 11px;
195 color: #555;
196 margin-right: 3px;
197 font-weight: 500;
198 }
199
200 .info-value {
201 font-size: 11px;
202 font-weight: 600;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000203 word-break: break-all;
Sean McCullough86b56862025-04-18 13:04:03 -0700204 }
205
Philip Zeyligerd1402952025-04-23 03:54:37 +0000206 [title] {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000207 cursor: default;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000208 }
209
Sean McCullough86b56862025-04-18 13:04:03 -0700210 .info-item a {
211 --tw-text-opacity: 1;
212 color: rgb(37 99 235 / var(--tw-text-opacity, 1));
213 text-decoration: inherit;
214 }
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000215
216 .info-toggle {
217 margin-left: 8px;
218 width: 24px;
219 height: 24px;
220 border-radius: 50%;
221 display: flex;
222 align-items: center;
223 justify-content: center;
224 background: #f0f0f0;
225 border: 1px solid #ddd;
226 cursor: pointer;
227 font-weight: bold;
228 font-style: italic;
229 color: #555;
230 transition: all 0.2s ease;
231 }
232
233 .info-toggle:hover {
234 background: #e0e0e0;
235 }
236
237 .info-toggle.active {
238 background: #4a90e2;
239 color: white;
240 border-color: #3a80d2;
241 }
242
243 .main-info-grid {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000244 display: grid;
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700245 grid-template-columns: 1fr 1fr;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000246 gap: 10px;
247 width: 100%;
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000248 }
249
250 .info-column {
251 display: flex;
252 flex-direction: column;
253 gap: 2px;
254 }
255
256 .detailed-info-grid {
257 display: grid;
258 grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
259 gap: 8px;
260 margin-top: 10px;
261 }
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000262
263 .ssh-section {
264 margin-top: 10px;
265 padding-top: 10px;
266 border-top: 1px solid #eee;
267 }
268
269 .ssh-command {
270 display: flex;
271 align-items: center;
272 margin-bottom: 8px;
273 gap: 10px;
274 }
275
276 .ssh-command-text {
277 font-family: monospace;
278 font-size: 12px;
279 background: #f5f5f5;
280 padding: 4px 8px;
281 border-radius: 4px;
282 border: 1px solid #e0e0e0;
283 flex-grow: 1;
284 }
285
286 .copy-button {
287 background: #f0f0f0;
288 border: 1px solid #ddd;
289 border-radius: 4px;
290 padding: 3px 6px;
291 font-size: 11px;
292 cursor: pointer;
293 transition: all 0.2s;
294 }
295
296 .copy-button:hover {
297 background: #e0e0e0;
298 }
299
300 .ssh-warning {
301 background: #fff3e0;
302 border-left: 3px solid #ff9800;
303 padding: 8px 12px;
304 margin-top: 8px;
305 font-size: 12px;
306 color: #e65100;
307 }
308
309 .vscode-link {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000310 color: white;
311 text-decoration: none;
312 background-color: #0066b8;
313 padding: 4px 8px;
314 border-radius: 4px;
315 display: flex;
316 align-items: center;
317 gap: 6px;
318 font-size: 12px;
319 transition: all 0.2s ease;
320 }
321
322 .vscode-link:hover {
323 background-color: #005091;
324 }
325
326 .vscode-icon {
327 width: 16px;
328 height: 16px;
329 }
330
331 .github-link {
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000332 color: #2962ff;
333 text-decoration: none;
334 }
335
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000336 .github-link:hover {
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000337 text-decoration: underline;
338 }
philip.zeyliger6d3de482025-06-10 19:38:14 -0700339
340 .commit-info-container {
341 display: flex;
342 align-items: center;
343 gap: 6px;
344 }
345
346 .commit-info-container .copy-icon {
347 opacity: 0.7;
348 display: flex;
349 align-items: center;
350 }
351
352 .commit-info-container .copy-icon svg {
353 vertical-align: middle;
354 }
355
356 .commit-info-container:hover .copy-icon {
357 opacity: 1;
358 }
359
360 .octocat-link {
361 color: #586069;
362 text-decoration: none;
363 display: flex;
364 align-items: center;
365 transition: color 0.2s ease;
366 }
367
368 .octocat-link:hover {
369 color: #0366d6;
370 }
371
372 .octocat-icon {
373 width: 16px;
374 height: 16px;
375 }
Sean McCullough86b56862025-04-18 13:04:03 -0700376 `;
377
378 constructor() {
379 super();
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000380 this._toggleInfoDetails = this._toggleInfoDetails.bind(this);
381
382 // Close the info panel when clicking outside of it
383 document.addEventListener("click", (event) => {
384 if (this.showDetails && !this.contains(event.target as Node)) {
385 this.showDetails = false;
386 this.requestUpdate();
387 }
388 });
389 }
390
391 /**
392 * Toggle the display of detailed information
393 */
394 private _toggleInfoDetails(event: Event) {
395 event.stopPropagation();
396 this.showDetails = !this.showDetails;
397 this.requestUpdate();
Sean McCullough86b56862025-04-18 13:04:03 -0700398 }
399
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000400 /**
401 * Update the last commit information based on messages
402 */
403 public updateLastCommitInfo(newMessages: AgentMessage[]): void {
404 if (!newMessages || newMessages.length === 0) return;
405
406 // Process messages in chronological order (latest last)
407 for (const message of newMessages) {
408 if (
409 message.type === "commit" &&
410 message.commits &&
411 message.commits.length > 0
412 ) {
413 // Get the first commit from the list
414 const commit = message.commits[0];
415 if (commit) {
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700416 // Check if the commit hash has changed
417 const hasChanged =
418 !this.lastCommit || this.lastCommit.hash !== commit.hash;
419
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000420 this.lastCommit = {
421 hash: commit.hash,
422 pushedBranch: commit.pushed_branch,
423 };
424 this.lastCommitCopied = false;
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700425
426 // Add pulse animation if the commit changed
427 if (hasChanged) {
428 // Find the last commit element
429 setTimeout(() => {
430 const lastCommitEl =
431 this.shadowRoot?.querySelector(".last-commit-main");
432 if (lastCommitEl) {
433 // Add the pulse class
434 lastCommitEl.classList.add("pulse");
435
436 // Remove the pulse class after animation completes
437 setTimeout(() => {
438 lastCommitEl.classList.remove("pulse");
439 }, 1500);
440 }
441 }, 0);
442 }
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000443 }
444 }
445 }
446 }
447
448 /**
449 * Copy commit info to clipboard when clicked
450 */
451 private copyCommitInfo(event: MouseEvent): void {
452 event.preventDefault();
453 event.stopPropagation();
454
455 if (!this.lastCommit) return;
456
457 const textToCopy =
458 this.lastCommit.pushedBranch || this.lastCommit.hash.substring(0, 8);
459
460 navigator.clipboard
461 .writeText(textToCopy)
462 .then(() => {
463 this.lastCommitCopied = true;
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700464 // Reset the copied state after 1.5 seconds
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000465 setTimeout(() => {
466 this.lastCommitCopied = false;
Philip Zeyliger9bca61e2025-05-22 12:40:06 -0700467 }, 1500);
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000468 })
469 .catch((err) => {
470 console.error("Failed to copy commit info:", err);
471 });
472 }
473
Philip Zeyligerd1402952025-04-23 03:54:37 +0000474 formatHostname() {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000475 // Only display outside hostname
Philip Zeyliger18532b22025-04-23 21:11:46 +0000476 const outsideHostname = this.state?.outside_hostname;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000477
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000478 if (!outsideHostname) {
Philip Zeyligerd1402952025-04-23 03:54:37 +0000479 return this.state?.hostname;
480 }
481
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000482 return outsideHostname;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000483 }
484
485 formatWorkingDir() {
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000486 // Only display outside working directory
Philip Zeyliger18532b22025-04-23 21:11:46 +0000487 const outsideWorkingDir = this.state?.outside_working_dir;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000488
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000489 if (!outsideWorkingDir) {
Philip Zeyligerd1402952025-04-23 03:54:37 +0000490 return this.state?.working_dir;
491 }
492
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000493 return outsideWorkingDir;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000494 }
495
496 getHostnameTooltip() {
Philip Zeyliger18532b22025-04-23 21:11:46 +0000497 const outsideHostname = this.state?.outside_hostname;
498 const insideHostname = this.state?.inside_hostname;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000499
500 if (
Philip Zeyliger18532b22025-04-23 21:11:46 +0000501 !outsideHostname ||
502 !insideHostname ||
503 outsideHostname === insideHostname
Philip Zeyligerd1402952025-04-23 03:54:37 +0000504 ) {
505 return "";
506 }
507
Philip Zeyliger18532b22025-04-23 21:11:46 +0000508 return `Outside: ${outsideHostname}, Inside: ${insideHostname}`;
509 }
510
511 getWorkingDirTooltip() {
512 const outsideWorkingDir = this.state?.outside_working_dir;
513 const insideWorkingDir = this.state?.inside_working_dir;
514
515 if (
516 !outsideWorkingDir ||
517 !insideWorkingDir ||
518 outsideWorkingDir === insideWorkingDir
519 ) {
520 return "";
521 }
522
523 return `Outside: ${outsideWorkingDir}, Inside: ${insideWorkingDir}`;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000524 }
525
Sean McCullough86b56862025-04-18 13:04:03 -0700526 // See https://lit.dev/docs/components/lifecycle/
527 connectedCallback() {
528 super.connectedCallback();
529 // register event listeners
530 }
531
532 // See https://lit.dev/docs/components/lifecycle/
533 disconnectedCallback() {
534 super.disconnectedCallback();
535 // unregister event listeners
536 }
537
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000538 copyToClipboard(text: string) {
539 navigator.clipboard
540 .writeText(text)
541 .then(() => {
542 // Could add a temporary success indicator here
543 })
544 .catch((err) => {
545 console.error("Could not copy text: ", err);
546 });
547 }
548
549 getSSHHostname() {
philip.zeyliger8773e682025-06-11 21:36:21 -0700550 // Use the ssh_connection_string from the state if available, otherwise fall back to generating it
551 return (
552 this.state?.ssh_connection_string || `sketch-${this.state?.session_id}`
553 );
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000554 }
555
philip.zeyligere8da7af2025-06-12 14:24:28 -0700556 getSSHConnectionString() {
557 // Return the connection string for VS Code remote SSH
558 const connectionString =
559 this.state?.ssh_connection_string || `sketch-${this.state?.session_id}`;
560 // If the connection string already contains user@, use it as-is
561 // Otherwise prepend root@ for VS Code remote SSH
562 if (connectionString.includes("@")) {
563 return connectionString;
564 } else {
565 return `root@${connectionString}`;
566 }
567 }
568
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000569 // Format GitHub repository URL to org/repo format
570 formatGitHubRepo(url) {
571 if (!url) return null;
572
573 // Common GitHub URL patterns
574 const patterns = [
575 // HTTPS URLs
Sean McCulloughc7c2cc12025-06-13 03:21:18 +0000576 /https:\/\/github\.com\/([^/]+)\/([^/\s]+?)(?:\.git)?$/,
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000577 // SSH URLs
Sean McCulloughc7c2cc12025-06-13 03:21:18 +0000578 /git@github\.com:([^/]+)\/([^/\s]+?)(?:\.git)?$/,
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000579 // Git protocol
Sean McCulloughc7c2cc12025-06-13 03:21:18 +0000580 /git:\/\/github\.com\/([^/]+)\/([^/\s]+?)(?:\.git)?$/,
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000581 ];
582
583 for (const pattern of patterns) {
584 const match = url.match(pattern);
585 if (match) {
586 return {
587 formatted: `${match[1]}/${match[2]}`,
588 url: `https://github.com/${match[1]}/${match[2]}`,
philip.zeyliger6d3de482025-06-10 19:38:14 -0700589 owner: match[1],
590 repo: match[2],
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000591 };
592 }
593 }
594
595 return null;
596 }
597
philip.zeyliger6d3de482025-06-10 19:38:14 -0700598 // Generate GitHub branch URL if linking is enabled
599 getGitHubBranchLink(branchName) {
600 if (!this.state?.link_to_github || !branchName) {
601 return null;
602 }
603
604 const github = this.formatGitHubRepo(this.state?.git_origin);
605 if (!github) {
606 return null;
607 }
608
609 return `https://github.com/${github.owner}/${github.repo}/tree/${branchName}`;
610 }
611
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000612 renderSSHSection() {
613 // Only show SSH section if we're in a Docker container and have session ID
614 if (!this.state?.session_id) {
615 return html``;
616 }
617
618 const sshHost = this.getSSHHostname();
philip.zeyligere8da7af2025-06-12 14:24:28 -0700619 const sshConnectionString = this.getSSHConnectionString();
620 const sshCommand = `ssh ${sshConnectionString}`;
621 const vscodeCommand = `code --remote ssh-remote+${sshConnectionString} /app -n`;
622 const vscodeURL = `vscode://vscode-remote/ssh-remote+${sshConnectionString}/app?windowId=_blank`;
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000623
624 if (!this.state?.ssh_available) {
625 return html`
626 <div class="ssh-section">
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000627 <h3>Connect to Container</h3>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000628 <div class="ssh-warning">
629 SSH connections are not available:
630 ${this.state?.ssh_error || "SSH configuration is missing"}
631 </div>
632 </div>
633 `;
634 }
635
636 return html`
637 <div class="ssh-section">
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000638 <h3>Connect to Container</h3>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000639 <div class="ssh-command">
640 <div class="ssh-command-text">${sshCommand}</div>
641 <button
642 class="copy-button"
643 @click=${() => this.copyToClipboard(sshCommand)}
644 >
645 Copy
646 </button>
647 </div>
648 <div class="ssh-command">
649 <div class="ssh-command-text">${vscodeCommand}</div>
650 <button
651 class="copy-button"
652 @click=${() => this.copyToClipboard(vscodeCommand)}
653 >
654 Copy
655 </button>
656 </div>
657 <div class="ssh-command">
Philip Zeyligerbce3a132025-04-30 22:03:39 +0000658 <a href="${vscodeURL}" class="vscode-link" title="${vscodeURL}">
659 <svg
660 class="vscode-icon"
661 xmlns="http://www.w3.org/2000/svg"
662 viewBox="0 0 24 24"
663 fill="none"
664 stroke="white"
665 stroke-width="2"
666 stroke-linecap="round"
667 stroke-linejoin="round"
668 >
669 <path
670 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"
671 />
672 </svg>
673 <span>Open in VSCode</span>
674 </a>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000675 </div>
676 </div>
677 `;
678 }
679
Sean McCullough86b56862025-04-18 13:04:03 -0700680 render() {
681 return html`
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000682 <div class="info-container">
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700683 <!-- Main visible info in two columns - github/hostname/dir and last commit -->
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000684 <div class="main-info-grid">
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700685 <!-- First column: GitHub repo (or hostname) and working dir -->
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000686 <div class="info-column">
687 <div class="info-item">
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700688 ${(() => {
689 const github = this.formatGitHubRepo(this.state?.git_origin);
690 if (github) {
691 return html`
692 <a
693 href="${github.url}"
694 target="_blank"
695 rel="noopener noreferrer"
696 class="github-link"
697 title="${this.state?.git_origin}"
698 >
699 ${github.formatted}
700 </a>
701 `;
702 } else {
703 return html`
704 <span
705 id="hostname"
706 class="info-value"
707 title="${this.getHostnameTooltip()}"
708 >
709 ${this.formatHostname()}
710 </span>
711 `;
712 }
713 })()}
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000714 </div>
715 <div class="info-item">
716 <span
717 id="workingDir"
718 class="info-value"
719 title="${this.getWorkingDirTooltip()}"
720 >
721 ${this.formatWorkingDir()}
722 </span>
723 </div>
724 </div>
725
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700726 <!-- Second column: Last Commit -->
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000727 <div class="info-column last-commit-column">
728 <div class="info-item">
729 <span class="info-label">Last Commit</span>
730 </div>
731 <div
732 class="info-item last-commit-main"
733 @click=${(e: MouseEvent) => this.copyCommitInfo(e)}
734 title="Click to copy"
735 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000736 ${this.lastCommit
737 ? this.lastCommit.pushedBranch
philip.zeyliger6d3de482025-06-10 19:38:14 -0700738 ? (() => {
739 const githubLink = this.getGitHubBranchLink(
740 this.lastCommit.pushedBranch,
741 );
742 return html`
743 <div class="commit-info-container">
744 <span
745 class="commit-branch-indicator main-grid-commit"
746 title="Click to copy: ${this.lastCommit
747 .pushedBranch}"
748 @click=${(e) => this.copyCommitInfo(e)}
749 >${this.lastCommit.pushedBranch}</span
750 >
751 <span class="copy-icon">
752 ${this.lastCommitCopied
753 ? html`<svg
754 xmlns="http://www.w3.org/2000/svg"
755 width="16"
756 height="16"
757 viewBox="0 0 24 24"
758 fill="none"
759 stroke="currentColor"
760 stroke-width="2"
761 stroke-linecap="round"
762 stroke-linejoin="round"
763 >
764 <path d="M20 6L9 17l-5-5"></path>
765 </svg>`
766 : html`<svg
767 xmlns="http://www.w3.org/2000/svg"
768 width="16"
769 height="16"
770 viewBox="0 0 24 24"
771 fill="none"
772 stroke="currentColor"
773 stroke-width="2"
774 stroke-linecap="round"
775 stroke-linejoin="round"
776 >
777 <rect
778 x="9"
779 y="9"
780 width="13"
781 height="13"
782 rx="2"
783 ry="2"
784 ></rect>
785 <path
786 d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
787 ></path>
788 </svg>`}
789 </span>
790 ${githubLink
791 ? html`<a
792 href="${githubLink}"
793 target="_blank"
794 rel="noopener noreferrer"
795 class="octocat-link"
796 title="Open ${this.lastCommit
797 .pushedBranch} on GitHub"
798 @click=${(e) => e.stopPropagation()}
799 >
800 <svg
801 class="octocat-icon"
802 viewBox="0 0 16 16"
803 width="16"
804 height="16"
805 >
806 <path
807 fill="currentColor"
808 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"
809 />
810 </svg>
811 </a>`
812 : ""}
813 </div>
814 `;
815 })()
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000816 : html`<span class="commit-hash-indicator main-grid-commit"
817 >${this.lastCommit.hash.substring(0, 8)}</span
818 >`
819 : html`<span class="no-commit-indicator">N/A</span>`}
820 </div>
821 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700822 </div>
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000823
824 <!-- Info toggle button -->
825 <button
826 class="info-toggle ${this.showDetails ? "active" : ""}"
827 @click=${this._toggleInfoDetails}
828 title="Show/hide details"
829 >
830 i
831 </button>
832
833 <!-- Expanded info panel -->
834 <div class="info-expanded ${this.showDetails ? "active" : ""}">
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000835 <!-- Last Commit section moved to main grid -->
836
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000837 <div class="detailed-info-grid">
838 <div class="info-item">
839 <span class="info-label">Commit:</span>
840 <span id="initialCommit" class="info-value"
841 >${this.state?.initial_commit?.substring(0, 8)}</span
842 >
843 </div>
844 <div class="info-item">
845 <span class="info-label">Msgs:</span>
846 <span id="messageCount" class="info-value"
847 >${this.state?.message_count}</span
848 >
849 </div>
850 <div class="info-item">
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000851 <span class="info-label">Session ID:</span>
852 <span id="sessionId" class="info-value"
853 >${this.state?.session_id || "N/A"}</span
854 >
855 </div>
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700856 <div class="info-item">
857 <span class="info-label">Hostname:</span>
858 <span
859 id="hostnameDetail"
860 class="info-value"
861 title="${this.getHostnameTooltip()}"
862 >
863 ${this.formatHostname()}
864 </span>
865 </div>
Philip Zeyliger72318392025-05-14 02:56:07 +0000866 ${this.state?.agent_state
867 ? html`
868 <div class="info-item">
869 <span class="info-label">Agent State:</span>
870 <span id="agentState" class="info-value"
871 >${this.state?.agent_state}</span
872 >
873 </div>
874 `
875 : ""}
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000876 <div class="info-item">
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000877 <span class="info-label">Input tokens:</span>
878 <span id="inputTokens" class="info-value"
879 >${formatNumber(
880 (this.state?.total_usage?.input_tokens || 0) +
881 (this.state?.total_usage?.cache_read_input_tokens || 0) +
882 (this.state?.total_usage?.cache_creation_input_tokens || 0),
883 )}</span
884 >
885 </div>
886 <div class="info-item">
887 <span class="info-label">Output tokens:</span>
888 <span id="outputTokens" class="info-value"
889 >${formatNumber(this.state?.total_usage?.output_tokens)}</span
890 >
891 </div>
Josh Bleecher Snyder44f847a2025-06-05 14:33:50 -0700892 ${(this.state?.total_usage?.total_cost_usd || 0) > 0
893 ? html`
894 <div class="info-item">
895 <span class="info-label">Total cost:</span>
896 <span id="totalCost" class="info-value cost"
897 >$${(this.state?.total_usage?.total_cost_usd).toFixed(
898 2,
899 )}</span
900 >
901 </div>
902 `
903 : ""}
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000904 <div
905 class="info-item"
906 style="grid-column: 1 / -1; margin-top: 5px; border-top: 1px solid #eee; padding-top: 5px;"
907 >
908 <a href="logs">Logs</a> (<a href="download">Download</a>)
909 </div>
910 </div>
Philip Zeyligerc72fff52025-04-29 20:17:54 +0000911
912 <!-- SSH Connection Information -->
913 ${this.renderSSHSection()}
Sean McCullough86b56862025-04-18 13:04:03 -0700914 </div>
915 </div>
916 `;
917 }
918}
919
920declare global {
921 interface HTMLElementTagNameMap {
922 "sketch-container-status": SketchContainerStatus;
923 }
924}