blob: 08ac605c13da1ee0585480e5477ef0a1150182ec [file] [log] [blame]
Sean McCullough86b56862025-04-18 13:04:03 -07001import { State } 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
Sean McCullough86b56862025-04-18 13:04:03 -070016 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
17 // Note that these styles only apply to the scope of this web component's
18 // shadow DOM node, so they won't leak out or collide with CSS declared in
19 // other components or the containing web page (...unless you want it to do that).
20 static styles = css`
Philip Zeyligere66db3e2025-04-27 15:40:39 +000021 .info-container {
22 display: flex;
23 align-items: center;
24 position: relative;
Sean McCullough86b56862025-04-18 13:04:03 -070025 }
26
27 .info-grid {
28 display: flex;
29 flex-wrap: wrap;
30 gap: 8px;
31 background: #f9f9f9;
32 border-radius: 4px;
33 padding: 4px 10px;
34 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
35 flex: 1;
36 }
37
Philip Zeyligere66db3e2025-04-27 15:40:39 +000038 .info-expanded {
39 position: absolute;
40 top: 100%;
41 right: 0;
42 z-index: 10;
43 min-width: 320px;
44 background: white;
45 border-radius: 8px;
46 padding: 10px 15px;
47 box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
48 margin-top: 5px;
49 display: none;
50 }
51
52 .info-expanded.active {
53 display: block;
54 }
55
Sean McCullough86b56862025-04-18 13:04:03 -070056 .info-item {
57 display: flex;
58 align-items: center;
59 white-space: nowrap;
60 margin-right: 10px;
61 font-size: 13px;
62 }
63
64 .info-label {
65 font-size: 11px;
66 color: #555;
67 margin-right: 3px;
68 font-weight: 500;
69 }
70
71 .info-value {
72 font-size: 11px;
73 font-weight: 600;
74 }
75
Philip Zeyligerd1402952025-04-23 03:54:37 +000076 [title] {
77 cursor: help;
78 text-decoration: underline dotted;
79 }
80
Sean McCullough86b56862025-04-18 13:04:03 -070081 .cost {
82 color: #2e7d32;
83 }
84
85 .info-item a {
86 --tw-text-opacity: 1;
87 color: rgb(37 99 235 / var(--tw-text-opacity, 1));
88 text-decoration: inherit;
89 }
Philip Zeyligere66db3e2025-04-27 15:40:39 +000090
91 .info-toggle {
92 margin-left: 8px;
93 width: 24px;
94 height: 24px;
95 border-radius: 50%;
96 display: flex;
97 align-items: center;
98 justify-content: center;
99 background: #f0f0f0;
100 border: 1px solid #ddd;
101 cursor: pointer;
102 font-weight: bold;
103 font-style: italic;
104 color: #555;
105 transition: all 0.2s ease;
106 }
107
108 .info-toggle:hover {
109 background: #e0e0e0;
110 }
111
112 .info-toggle.active {
113 background: #4a90e2;
114 color: white;
115 border-color: #3a80d2;
116 }
117
118 .main-info-grid {
119 display: flex;
120 gap: 20px;
121 }
122
123 .info-column {
124 display: flex;
125 flex-direction: column;
126 gap: 2px;
127 }
128
129 .detailed-info-grid {
130 display: grid;
131 grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
132 gap: 8px;
133 margin-top: 10px;
134 }
Sean McCullough86b56862025-04-18 13:04:03 -0700135 `;
136
137 constructor() {
138 super();
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000139 this._toggleInfoDetails = this._toggleInfoDetails.bind(this);
140
141 // Close the info panel when clicking outside of it
142 document.addEventListener("click", (event) => {
143 if (this.showDetails && !this.contains(event.target as Node)) {
144 this.showDetails = false;
145 this.requestUpdate();
146 }
147 });
148 }
149
150 /**
151 * Toggle the display of detailed information
152 */
153 private _toggleInfoDetails(event: Event) {
154 event.stopPropagation();
155 this.showDetails = !this.showDetails;
156 this.requestUpdate();
Sean McCullough86b56862025-04-18 13:04:03 -0700157 }
158
Philip Zeyligerd1402952025-04-23 03:54:37 +0000159 formatHostname() {
Philip Zeyliger18532b22025-04-23 21:11:46 +0000160 const outsideHostname = this.state?.outside_hostname;
161 const insideHostname = this.state?.inside_hostname;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000162
Philip Zeyliger18532b22025-04-23 21:11:46 +0000163 if (!outsideHostname || !insideHostname) {
Philip Zeyligerd1402952025-04-23 03:54:37 +0000164 return this.state?.hostname;
165 }
166
Philip Zeyliger18532b22025-04-23 21:11:46 +0000167 if (outsideHostname === insideHostname) {
168 return outsideHostname;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000169 }
170
Philip Zeyliger18532b22025-04-23 21:11:46 +0000171 return `${outsideHostname}:${insideHostname}`;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000172 }
173
174 formatWorkingDir() {
Philip Zeyliger18532b22025-04-23 21:11:46 +0000175 const outsideWorkingDir = this.state?.outside_working_dir;
176 const insideWorkingDir = this.state?.inside_working_dir;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000177
Philip Zeyliger18532b22025-04-23 21:11:46 +0000178 if (!outsideWorkingDir || !insideWorkingDir) {
Philip Zeyligerd1402952025-04-23 03:54:37 +0000179 return this.state?.working_dir;
180 }
181
Philip Zeyliger18532b22025-04-23 21:11:46 +0000182 if (outsideWorkingDir === insideWorkingDir) {
183 return outsideWorkingDir;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000184 }
185
Philip Zeyliger18532b22025-04-23 21:11:46 +0000186 return `${outsideWorkingDir}:${insideWorkingDir}`;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000187 }
188
189 getHostnameTooltip() {
Philip Zeyliger18532b22025-04-23 21:11:46 +0000190 const outsideHostname = this.state?.outside_hostname;
191 const insideHostname = this.state?.inside_hostname;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000192
193 if (
Philip Zeyliger18532b22025-04-23 21:11:46 +0000194 !outsideHostname ||
195 !insideHostname ||
196 outsideHostname === insideHostname
Philip Zeyligerd1402952025-04-23 03:54:37 +0000197 ) {
198 return "";
199 }
200
Philip Zeyliger18532b22025-04-23 21:11:46 +0000201 return `Outside: ${outsideHostname}, Inside: ${insideHostname}`;
202 }
203
204 getWorkingDirTooltip() {
205 const outsideWorkingDir = this.state?.outside_working_dir;
206 const insideWorkingDir = this.state?.inside_working_dir;
207
208 if (
209 !outsideWorkingDir ||
210 !insideWorkingDir ||
211 outsideWorkingDir === insideWorkingDir
212 ) {
213 return "";
214 }
215
216 return `Outside: ${outsideWorkingDir}, Inside: ${insideWorkingDir}`;
Philip Zeyligerd1402952025-04-23 03:54:37 +0000217 }
218
Sean McCullough86b56862025-04-18 13:04:03 -0700219 // See https://lit.dev/docs/components/lifecycle/
220 connectedCallback() {
221 super.connectedCallback();
222 // register event listeners
223 }
224
225 // See https://lit.dev/docs/components/lifecycle/
226 disconnectedCallback() {
227 super.disconnectedCallback();
228 // unregister event listeners
229 }
230
231 render() {
232 return html`
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000233 <div class="info-container">
234 <!-- Main visible info in two columns - hostname/dir and repo/cost -->
235 <div class="main-info-grid">
236 <!-- First column: hostname and working dir -->
237 <div class="info-column">
238 <div class="info-item">
239 <span
240 id="hostname"
241 class="info-value"
242 title="${this.getHostnameTooltip()}"
243 >
244 ${this.formatHostname()}
245 </span>
246 </div>
247 <div class="info-item">
248 <span
249 id="workingDir"
250 class="info-value"
251 title="${this.getWorkingDirTooltip()}"
252 >
253 ${this.formatWorkingDir()}
254 </span>
255 </div>
256 </div>
257
258 <!-- Second column: git repo and cost -->
259 <div class="info-column">
260 ${this.state?.git_origin
261 ? html`
262 <div class="info-item">
263 <span id="gitOrigin" class="info-value"
264 >${this.state?.git_origin}</span
265 >
266 </div>
267 `
268 : ""}
269 <div class="info-item">
270 <span id="totalCost" class="info-value cost"
271 >$${(this.state?.total_usage?.total_cost_usd || 0).toFixed(
272 2,
273 )}</span
274 >
275 </div>
276 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700277 </div>
Philip Zeyligere66db3e2025-04-27 15:40:39 +0000278
279 <!-- Info toggle button -->
280 <button
281 class="info-toggle ${this.showDetails ? "active" : ""}"
282 @click=${this._toggleInfoDetails}
283 title="Show/hide details"
284 >
285 i
286 </button>
287
288 <!-- Expanded info panel -->
289 <div class="info-expanded ${this.showDetails ? "active" : ""}">
290 <div class="detailed-info-grid">
291 <div class="info-item">
292 <span class="info-label">Commit:</span>
293 <span id="initialCommit" class="info-value"
294 >${this.state?.initial_commit?.substring(0, 8)}</span
295 >
296 </div>
297 <div class="info-item">
298 <span class="info-label">Msgs:</span>
299 <span id="messageCount" class="info-value"
300 >${this.state?.message_count}</span
301 >
302 </div>
303 <div class="info-item">
304 <span class="info-label">Input tokens:</span>
305 <span id="inputTokens" class="info-value"
306 >${formatNumber(
307 (this.state?.total_usage?.input_tokens || 0) +
308 (this.state?.total_usage?.cache_read_input_tokens || 0) +
309 (this.state?.total_usage?.cache_creation_input_tokens || 0),
310 )}</span
311 >
312 </div>
313 <div class="info-item">
314 <span class="info-label">Output tokens:</span>
315 <span id="outputTokens" class="info-value"
316 >${formatNumber(this.state?.total_usage?.output_tokens)}</span
317 >
318 </div>
319 <div
320 class="info-item"
321 style="grid-column: 1 / -1; margin-top: 5px; border-top: 1px solid #eee; padding-top: 5px;"
322 >
323 <a href="logs">Logs</a> (<a href="download">Download</a>)
324 </div>
325 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700326 </div>
327 </div>
328 `;
329 }
330}
331
332declare global {
333 interface HTMLElementTagNameMap {
334 "sketch-container-status": SketchContainerStatus;
335 }
336}