blob: 26246b29af6c43f251be0a34bfa927368861459c [file] [log] [blame]
philip.zeyliger26bc6592025-06-30 20:15:30 -07001/* eslint-disable @typescript-eslint/no-explicit-any */
bankseanc5147482025-06-29 00:41:58 +00002import { html, render } from "lit";
Sean McCullough86b56862025-04-18 13:04:03 -07003import { unsafeHTML } from "lit/directives/unsafe-html.js";
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00004import { customElement, property, state } from "lit/decorators.js";
philip.zeyliger6d3de482025-06-10 19:38:14 -07005import { AgentMessage, State } from "../types";
Sean McCullough8d93e362025-04-27 23:32:18 +00006import { marked, MarkedOptions, Renderer, Tokens } from "marked";
philip.zeyliger7c1a6872025-06-16 03:54:37 +00007import type mermaid from "mermaid";
Philip Zeyliger53ab2452025-06-04 17:49:33 +00008import DOMPurify from "dompurify";
philip.zeyliger7c1a6872025-06-16 03:54:37 +00009
10// Mermaid is loaded dynamically - see loadMermaid() function
11declare global {
12 interface Window {
13 mermaid?: typeof mermaid;
14 }
15}
16
17// Mermaid hash will be injected at build time
18declare const __MERMAID_HASH__: string;
19
20// Load Mermaid dynamically
21let mermaidLoadPromise: Promise<any> | null = null;
22
23function loadMermaid(): Promise<typeof mermaid> {
24 if (mermaidLoadPromise) {
25 return mermaidLoadPromise;
26 }
27
28 if (window.mermaid) {
29 return Promise.resolve(window.mermaid);
30 }
31
32 mermaidLoadPromise = new Promise((resolve, reject) => {
33 // Get the Mermaid hash from build-time constant
34 const mermaidHash = __MERMAID_HASH__;
35
36 // Try to load the external Mermaid bundle
37 const script = document.createElement("script");
38 script.onload = () => {
39 // The Mermaid bundle should set window.mermaid
40 if (window.mermaid) {
41 resolve(window.mermaid);
42 } else {
43 reject(new Error("Mermaid not loaded from external bundle"));
44 }
45 };
46 script.onerror = (error) => {
47 console.warn("Failed to load external Mermaid bundle:", error);
48 reject(new Error("Mermaid external bundle failed to load"));
49 };
50
51 // Don't set type="module" since we're using IIFE format
52 script.src = `./static/mermaid-standalone-${mermaidHash}.js`;
53 document.head.appendChild(script);
54 });
55
56 return mermaidLoadPromise;
57}
Sean McCullough86b56862025-04-18 13:04:03 -070058import "./sketch-tool-calls";
bankseanc5147482025-06-29 00:41:58 +000059import { SketchTailwindElement } from "./sketch-tailwind-element";
60
Sean McCullough86b56862025-04-18 13:04:03 -070061@customElement("sketch-timeline-message")
bankseanc5147482025-06-29 00:41:58 +000062export class SketchTimelineMessage extends SketchTailwindElement {
Sean McCullough86b56862025-04-18 13:04:03 -070063 @property()
Sean McCulloughd9f13372025-04-21 15:08:49 -070064 message: AgentMessage;
Sean McCullough86b56862025-04-18 13:04:03 -070065
66 @property()
philip.zeyliger6d3de482025-06-10 19:38:14 -070067 state: State;
68
69 @property()
Sean McCulloughd9f13372025-04-21 15:08:49 -070070 previousMessage: AgentMessage;
Sean McCullough86b56862025-04-18 13:04:03 -070071
Sean McCullough2deac842025-04-21 18:17:57 -070072 @property()
73 open: boolean = false;
74
Philip Zeyligerb8a8f352025-06-02 07:39:37 -070075 @property()
76 firstMessageIndex: number = 0;
77
David Crawshaw4b644682025-06-26 17:15:10 +000078 @property({ type: Boolean, reflect: true, attribute: "compactpadding" })
79 compactPadding: boolean = false;
80
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000081 @state()
82 showInfo: boolean = false;
83
bankseanc5147482025-06-29 00:41:58 +000084 // Styles have been converted to Tailwind classes applied directly to HTML elements
85 // since this component now extends SketchTailwindElement which disables shadow DOM
Sean McCullough86b56862025-04-18 13:04:03 -070086
Sean McCullough8d93e362025-04-27 23:32:18 +000087 // Track mermaid diagrams that need rendering
88 private mermaidDiagrams = new Map();
89
Sean McCullough86b56862025-04-18 13:04:03 -070090 constructor() {
91 super();
philip.zeyliger7c1a6872025-06-16 03:54:37 +000092 // Mermaid will be initialized lazily when first needed
Sean McCullough86b56862025-04-18 13:04:03 -070093 }
94
95 // See https://lit.dev/docs/components/lifecycle/
96 connectedCallback() {
97 super.connectedCallback();
bankseanc5147482025-06-29 00:41:58 +000098 this.ensureGlobalStyles();
99 }
100
101 // Ensure global styles are injected when component is used
102 private ensureGlobalStyles() {
103 if (!document.querySelector("#sketch-timeline-message-styles")) {
104 const floatingMessageStyles = document.createElement("style");
105 floatingMessageStyles.id = "sketch-timeline-message-styles";
106 floatingMessageStyles.textContent = this.getGlobalStylesContent();
107 document.head.appendChild(floatingMessageStyles);
108 }
109 }
110
111 // Get the global styles content
112 private getGlobalStylesContent(): string {
113 return `
114 .floating-message {
115 background-color: rgba(31, 41, 55, 1);
116 color: white;
117 padding: 4px 10px;
118 border-radius: 4px;
119 font-size: 12px;
120 font-family: system-ui, sans-serif;
121 box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
122 pointer-events: none;
123 transition: all 0.3s ease;
124 }
125
126 .floating-message.success {
127 background-color: rgba(34, 197, 94, 0.9);
128 }
129
130 .floating-message.error {
131 background-color: rgba(239, 68, 68, 0.9);
132 }
133
134 /* Comprehensive markdown content styling */
135 .markdown-content h1 {
136 font-size: 1.875rem;
137 font-weight: 700;
138 margin: 1rem 0 0.5rem 0;
139 line-height: 1.25;
140 }
141
142 .markdown-content h2 {
143 font-size: 1.5rem;
144 font-weight: 600;
145 margin: 0.875rem 0 0.5rem 0;
146 line-height: 1.25;
147 }
148
149 .markdown-content h3 {
150 font-size: 1.25rem;
151 font-weight: 600;
152 margin: 0.75rem 0 0.375rem 0;
153 line-height: 1.375;
154 }
155
156 .markdown-content h4 {
157 font-size: 1.125rem;
158 font-weight: 600;
159 margin: 0.625rem 0 0.375rem 0;
160 line-height: 1.375;
161 }
162
163 .markdown-content h5 {
164 font-size: 1rem;
165 font-weight: 600;
166 margin: 0.5rem 0 0.25rem 0;
167 line-height: 1.5;
168 }
169
170 .markdown-content h6 {
171 font-size: 0.875rem;
172 font-weight: 600;
173 margin: 0.5rem 0 0.25rem 0;
174 line-height: 1.5;
175 }
176
177 .markdown-content h1:first-child,
178 .markdown-content h2:first-child,
179 .markdown-content h3:first-child,
180 .markdown-content h4:first-child,
181 .markdown-content h5:first-child,
182 .markdown-content h6:first-child {
183 margin-top: 0;
184 }
185
186 .markdown-content p {
187 margin: 0.25rem 0;
188 }
189
190 .markdown-content p:first-child {
191 margin-top: 0;
192 }
193
194 .markdown-content p:last-child {
195 margin-bottom: 0;
196 }
197
198 .markdown-content a {
199 color: inherit;
200 text-decoration: underline;
201 }
202
203 .markdown-content ul,
204 .markdown-content ol {
205 padding-left: 1.5rem;
206 margin: 0.5rem 0;
207 }
208
209 .markdown-content ul {
210 list-style-type: disc;
211 }
212
213 .markdown-content ol {
214 list-style-type: decimal;
215 }
216
217 .markdown-content li {
218 margin: 0.25rem 0;
219 }
220
221 .markdown-content blockquote {
222 border-left: 3px solid rgba(0, 0, 0, 0.2);
223 padding-left: 1rem;
224 margin-left: 0.5rem;
225 font-style: italic;
226 color: rgba(0, 0, 0, 0.7);
227 }
228
229 .markdown-content strong {
230 font-weight: 700;
231 }
232
233 .markdown-content em {
234 font-style: italic;
235 }
236
237 .markdown-content hr {
238 border: none;
239 border-top: 1px solid rgba(0, 0, 0, 0.1);
240 margin: 1rem 0;
241 }
242
243 /* User message specific markdown styling */
244 sketch-timeline-message .bg-blue-500 .markdown-content a {
245 color: #fff;
246 text-decoration: underline;
247 }
248
249 sketch-timeline-message .bg-blue-500 .markdown-content blockquote {
250 border-left: 3px solid rgba(255, 255, 255, 0.4);
251 color: rgba(255, 255, 255, 0.9);
252 }
253
254 sketch-timeline-message .bg-blue-500 .markdown-content hr {
255 border-top: 1px solid rgba(255, 255, 255, 0.3);
256 }
257
258 /* Code block styling within markdown */
259 .markdown-content pre,
260 .markdown-content code {
261 font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
262 background: rgba(0, 0, 0, 0.05);
263 border-radius: 4px;
264 padding: 2px 4px;
265 overflow-x: auto;
266 max-width: 100%;
267 word-break: break-all;
268 box-sizing: border-box;
269 }
270
271 .markdown-content pre {
272 padding: 8px 12px;
273 margin: 0.5rem 0;
274 line-height: 1.4;
275 }
276
277 .markdown-content pre code {
278 background: transparent;
279 padding: 0;
280 }
281
282 /* User message code styling */
283 sketch-timeline-message .bg-blue-500 .markdown-content pre,
284 sketch-timeline-message .bg-blue-500 .markdown-content code {
285 background: rgba(255, 255, 255, 0.2);
286 color: white;
287 }
288
289 sketch-timeline-message .bg-blue-500 .markdown-content pre code {
290 background: transparent;
291 }
292
293 /* Code block containers */
294 .code-block-container {
295 position: relative;
296 margin: 8px 0;
297 border-radius: 6px;
298 overflow: hidden;
299 background: rgba(0, 0, 0, 0.05);
300 }
301
302 sketch-timeline-message .bg-blue-500 .code-block-container {
303 background: rgba(255, 255, 255, 0.2);
304 }
305
306 .code-block-header {
307 display: flex;
308 justify-content: space-between;
309 align-items: center;
310 padding: 4px 8px;
311 background: rgba(0, 0, 0, 0.1);
312 font-size: 12px;
313 }
314
315 sketch-timeline-message .bg-blue-500 .code-block-header {
316 background: rgba(255, 255, 255, 0.2);
317 color: white;
318 }
319
320 .code-language {
321 font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
322 font-size: 11px;
323 font-weight: 500;
324 }
325
326 .code-copy-button {
327 background: transparent;
328 border: none;
329 cursor: pointer;
330 padding: 2px;
331 border-radius: 3px;
332 display: flex;
333 align-items: center;
334 justify-content: center;
335 opacity: 0.7;
336 transition: all 0.15s ease;
337 }
338
339 .code-copy-button:hover {
340 opacity: 1;
341 background: rgba(0, 0, 0, 0.1);
342 }
343
344 sketch-timeline-message .bg-blue-500 .code-copy-button:hover {
345 background: rgba(255, 255, 255, 0.2);
346 }
347
348 .code-block-container pre {
349 margin: 0;
350 padding: 8px;
351 background: transparent;
352 }
353
354 .code-block-container code {
355 background: transparent;
356 padding: 0;
357 display: block;
358 width: 100%;
359 }
360
361 /* Mermaid diagram styling */
362 .mermaid-container {
363 margin: 1rem 0;
364 padding: 0.5rem;
365 background-color: #f8f8f8;
366 border-radius: 4px;
367 overflow-x: auto;
368 }
369
370 .mermaid {
371 text-align: center;
372 }
373
374 /* Print styles */
375 @media print {
376 .floating-message,
377 .commit-diff-button,
378 button[title="Copy to clipboard"],
379 button[title="Show message details"] {
380 display: none !important;
381 }
382 }
383`;
Sean McCullough86b56862025-04-18 13:04:03 -0700384 }
Autoformatterdded2d62025-04-28 00:27:21 +0000385
Sean McCullough8d93e362025-04-27 23:32:18 +0000386 // After the component is updated and rendered, render any mermaid diagrams
387 updated(changedProperties: Map<string, unknown>) {
388 super.updated(changedProperties);
389 this.renderMermaidDiagrams();
Pokey Rulea10f1512025-05-15 13:53:26 +0000390 this.setupCodeBlockCopyButtons();
Sean McCullough8d93e362025-04-27 23:32:18 +0000391 }
Autoformatterdded2d62025-04-28 00:27:21 +0000392
Sean McCullough8d93e362025-04-27 23:32:18 +0000393 // Render mermaid diagrams after the component is updated
394 renderMermaidDiagrams() {
395 // Add a small delay to ensure the DOM is fully rendered
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000396 setTimeout(async () => {
Sean McCullough8d93e362025-04-27 23:32:18 +0000397 // Find all mermaid containers in our shadow root
Autoformatterdded2d62025-04-28 00:27:21 +0000398 const containers = this.shadowRoot?.querySelectorAll(".mermaid");
Sean McCullough8d93e362025-04-27 23:32:18 +0000399 if (!containers || containers.length === 0) return;
Autoformatterdded2d62025-04-28 00:27:21 +0000400
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000401 try {
402 // Load mermaid dynamically
403 const mermaidLib = await loadMermaid();
Autoformatterdded2d62025-04-28 00:27:21 +0000404
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000405 // Initialize mermaid with specific config (only once per load)
406 mermaidLib.initialize({
407 startOnLoad: false,
408 suppressErrorRendering: true,
409 theme: "default",
410 securityLevel: "loose", // Allows more flexibility but be careful with user-generated content
411 fontFamily: "monospace",
412 });
Autoformatterdded2d62025-04-28 00:27:21 +0000413
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000414 // Process each mermaid diagram
415 containers.forEach((container) => {
416 const id = container.id;
417 const code = container.textContent || "";
418 if (!code || !id) return; // Use return for forEach instead of continue
419
420 try {
421 // Clear any previous content
422 container.innerHTML = code;
423
424 // Render the mermaid diagram using promise
425 mermaidLib
426 .render(`${id}-svg`, code)
427 .then(({ svg }) => {
428 container.innerHTML = svg;
429 })
430 .catch((err) => {
431 console.error("Error rendering mermaid diagram:", err);
432 // Show the original code as fallback
433 container.innerHTML = `<pre>${code}</pre>`;
434 });
435 } catch (err) {
436 console.error("Error processing mermaid diagram:", err);
437 // Show the original code as fallback
438 container.innerHTML = `<pre>${code}</pre>`;
439 }
440 });
441 } catch (err) {
442 console.error("Error loading mermaid:", err);
443 // Show the original code as fallback for all diagrams
444 containers.forEach((container) => {
445 const code = container.textContent || "";
Sean McCullough8d93e362025-04-27 23:32:18 +0000446 container.innerHTML = `<pre>${code}</pre>`;
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000447 });
448 }
Sean McCullough8d93e362025-04-27 23:32:18 +0000449 }, 100); // Small delay to ensure DOM is ready
450 }
Sean McCullough86b56862025-04-18 13:04:03 -0700451
Pokey Rulea10f1512025-05-15 13:53:26 +0000452 // Setup code block copy buttons after component is updated
453 setupCodeBlockCopyButtons() {
454 setTimeout(() => {
455 // Find all copy buttons in code blocks
456 const copyButtons =
457 this.shadowRoot?.querySelectorAll(".code-copy-button");
458 if (!copyButtons || copyButtons.length === 0) return;
459
460 // Add click event listener to each button
461 copyButtons.forEach((button) => {
462 button.addEventListener("click", (e) => {
463 e.stopPropagation();
464 const codeId = (button as HTMLElement).dataset.codeId;
465 if (!codeId) return;
466
467 const codeElement = this.shadowRoot?.querySelector(`#${codeId}`);
468 if (!codeElement) return;
469
470 const codeText = codeElement.textContent || "";
471 const buttonRect = button.getBoundingClientRect();
472
473 // Copy code to clipboard
474 navigator.clipboard
475 .writeText(codeText)
476 .then(() => {
477 // Show success indicator
478 const originalHTML = button.innerHTML;
479 button.innerHTML = `
480 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
481 <path d="M20 6L9 17l-5-5"></path>
482 </svg>
483 `;
484
485 // Display floating message
486 this.showFloatingMessage("Copied!", buttonRect, "success");
487
488 // Reset button after delay
489 setTimeout(() => {
490 button.innerHTML = originalHTML;
491 }, 2000);
492 })
493 .catch((err) => {
494 console.error("Failed to copy code:", err);
495 this.showFloatingMessage("Failed to copy!", buttonRect, "error");
496 });
497 });
498 });
499 }, 100); // Small delay to ensure DOM is ready
500 }
501
Sean McCullough86b56862025-04-18 13:04:03 -0700502 // See https://lit.dev/docs/components/lifecycle/
503 disconnectedCallback() {
504 super.disconnectedCallback();
505 }
506
507 renderMarkdown(markdownContent: string): string {
508 try {
Sean McCullough8d93e362025-04-27 23:32:18 +0000509 // Create a custom renderer
510 const renderer = new Renderer();
511 const originalCodeRenderer = renderer.code.bind(renderer);
Autoformatterdded2d62025-04-28 00:27:21 +0000512
Pokey Rulea10f1512025-05-15 13:53:26 +0000513 // Override the code renderer to handle mermaid diagrams and add copy buttons
Autoformatterdded2d62025-04-28 00:27:21 +0000514 renderer.code = function ({ text, lang, escaped }: Tokens.Code): string {
515 if (lang === "mermaid") {
Sean McCullough8d93e362025-04-27 23:32:18 +0000516 // Generate a unique ID for this diagram
517 const id = `mermaid-diagram-${Math.random().toString(36).substring(2, 10)}`;
Autoformatterdded2d62025-04-28 00:27:21 +0000518
Sean McCullough8d93e362025-04-27 23:32:18 +0000519 // Just create the container and mermaid div - we'll render it in the updated() lifecycle method
520 return `<div class="mermaid-container">
521 <div class="mermaid" id="${id}">${text}</div>
522 </div>`;
523 }
Pokey Rulea10f1512025-05-15 13:53:26 +0000524
Philip Zeyliger0d092842025-06-09 18:57:12 -0700525 // For regular code blocks, call the original renderer to get properly escaped HTML
526 const originalCodeHtml = originalCodeRenderer({ text, lang, escaped });
527
528 // Extract the code content from the original HTML to add our custom wrapper
529 // The original renderer returns: <pre><code class="language-x">escapedText</code></pre>
530 const codeMatch = originalCodeHtml.match(
531 /<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/,
532 );
533 if (!codeMatch) {
534 // Fallback to original if we can't parse it
535 return originalCodeHtml;
536 }
537
538 const escapedText = codeMatch[1];
Pokey Rulea10f1512025-05-15 13:53:26 +0000539 const id = `code-block-${Math.random().toString(36).substring(2, 10)}`;
540 const langClass = lang ? ` class="language-${lang}"` : "";
541
542 return `<div class="code-block-container">
543 <div class="code-block-header">
544 ${lang ? `<span class="code-language">${lang}</span>` : ""}
545 <button class="code-copy-button" title="Copy code" data-code-id="${id}">
546 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
547 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
548 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
549 </svg>
550 </button>
551 </div>
Philip Zeyliger0d092842025-06-09 18:57:12 -0700552 <pre><code id="${id}"${langClass}>${escapedText}</code></pre>
Pokey Rulea10f1512025-05-15 13:53:26 +0000553 </div>`;
Sean McCullough8d93e362025-04-27 23:32:18 +0000554 };
Autoformatterdded2d62025-04-28 00:27:21 +0000555
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000556 // Set markdown options for proper code block highlighting
Sean McCullough86b56862025-04-18 13:04:03 -0700557 const markedOptions: MarkedOptions = {
558 gfm: true, // GitHub Flavored Markdown
559 breaks: true, // Convert newlines to <br>
560 async: false,
Autoformatterdded2d62025-04-28 00:27:21 +0000561 renderer: renderer,
Sean McCullough86b56862025-04-18 13:04:03 -0700562 };
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000563
564 // Parse markdown and sanitize the output HTML with DOMPurify
565 const htmlOutput = marked.parse(markdownContent, markedOptions) as string;
566 return DOMPurify.sanitize(htmlOutput, {
567 // Allow common HTML elements that are safe
568 ALLOWED_TAGS: [
569 "p",
570 "br",
571 "strong",
572 "em",
573 "b",
574 "i",
575 "u",
576 "s",
577 "code",
578 "pre",
579 "h1",
580 "h2",
581 "h3",
582 "h4",
583 "h5",
584 "h6",
585 "ul",
586 "ol",
587 "li",
588 "blockquote",
589 "a",
590 "div",
591 "span", // For mermaid diagrams and code blocks
592 "svg",
593 "g",
594 "path",
595 "rect",
596 "circle",
597 "text",
598 "line",
599 "polygon", // For mermaid SVG
600 "button", // For code copy buttons
601 ],
602 ALLOWED_ATTR: [
603 "href",
604 "title",
605 "target",
606 "rel", // For links
607 "class",
608 "id", // For styling and functionality
609 "data-*", // For code copy buttons
610 // SVG attributes for mermaid diagrams
611 "viewBox",
612 "width",
613 "height",
614 "xmlns",
615 "fill",
616 "stroke",
617 "stroke-width",
618 "d",
619 "x",
620 "y",
621 "x1",
622 "y1",
623 "x2",
624 "y2",
625 "cx",
626 "cy",
627 "r",
628 "rx",
629 "ry",
630 "points",
631 "transform",
632 "text-anchor",
633 "font-size",
634 "font-family",
635 ],
636 // Allow data attributes for functionality
637 ALLOW_DATA_ATTR: true,
638 // Keep whitespace for code formatting
639 KEEP_CONTENT: true,
640 });
Sean McCullough86b56862025-04-18 13:04:03 -0700641 } catch (error) {
642 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000643 // Fallback to sanitized plain text if markdown parsing fails
644 return DOMPurify.sanitize(markdownContent);
Sean McCullough86b56862025-04-18 13:04:03 -0700645 }
646 }
647
648 /**
649 * Format timestamp for display
650 */
651 formatTimestamp(
652 timestamp: string | number | Date | null | undefined,
653 defaultValue: string = "",
654 ): string {
655 if (!timestamp) return defaultValue;
656 try {
657 const date = new Date(timestamp);
658 if (isNaN(date.getTime())) return defaultValue;
659
660 // Format: Mar 13, 2025 09:53:25 AM
661 return date.toLocaleString("en-US", {
662 month: "short",
663 day: "numeric",
664 year: "numeric",
665 hour: "numeric",
666 minute: "2-digit",
667 second: "2-digit",
668 hour12: true,
669 });
philip.zeyliger26bc6592025-06-30 20:15:30 -0700670 } catch {
Sean McCullough86b56862025-04-18 13:04:03 -0700671 return defaultValue;
672 }
673 }
674
675 formatNumber(
676 num: number | null | undefined,
677 defaultValue: string = "0",
678 ): string {
679 if (num === undefined || num === null) return defaultValue;
680 try {
681 return num.toLocaleString();
philip.zeyliger26bc6592025-06-30 20:15:30 -0700682 } catch {
Sean McCullough86b56862025-04-18 13:04:03 -0700683 return String(num);
684 }
685 }
686 formatCurrency(
687 num: number | string | null | undefined,
688 defaultValue: string = "$0.00",
689 isMessageLevel: boolean = false,
690 ): string {
691 if (num === undefined || num === null) return defaultValue;
692 try {
693 // Use 4 decimal places for message-level costs, 2 for totals
694 const decimalPlaces = isMessageLevel ? 4 : 2;
695 return `$${parseFloat(String(num)).toFixed(decimalPlaces)}`;
philip.zeyliger26bc6592025-06-30 20:15:30 -0700696 } catch {
Sean McCullough86b56862025-04-18 13:04:03 -0700697 return defaultValue;
698 }
699 }
700
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000701 // Format duration from nanoseconds to a human-readable string
702 _formatDuration(nanoseconds: number | null | undefined): string {
703 if (!nanoseconds) return "0s";
704
705 const seconds = nanoseconds / 1e9;
706
707 if (seconds < 60) {
708 return `${seconds.toFixed(1)}s`;
709 } else if (seconds < 3600) {
710 const minutes = Math.floor(seconds / 60);
711 const remainingSeconds = seconds % 60;
712 return `${minutes}min ${remainingSeconds.toFixed(0)}s`;
713 } else {
714 const hours = Math.floor(seconds / 3600);
715 const remainingSeconds = seconds % 3600;
716 const minutes = Math.floor(remainingSeconds / 60);
717 return `${hours}h ${minutes}min`;
718 }
719 }
720
Sean McCullough86b56862025-04-18 13:04:03 -0700721 showCommit(commitHash: string) {
Sean McCullough71941bd2025-04-18 13:31:48 -0700722 this.dispatchEvent(
723 new CustomEvent("show-commit-diff", {
724 bubbles: true,
725 composed: true,
726 detail: { commitHash },
727 }),
728 );
Sean McCullough86b56862025-04-18 13:04:03 -0700729 }
730
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000731 _toggleInfo(e: Event) {
732 e.stopPropagation();
733 this.showInfo = !this.showInfo;
734 }
735
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000736 copyToClipboard(text: string, event: Event) {
737 const element = event.currentTarget as HTMLElement;
738 const rect = element.getBoundingClientRect();
739
740 navigator.clipboard
741 .writeText(text)
742 .then(() => {
743 this.showFloatingMessage("Copied!", rect, "success");
744 })
745 .catch((err) => {
746 console.error("Failed to copy text: ", err);
747 this.showFloatingMessage("Failed to copy!", rect, "error");
748 });
749 }
750
751 showFloatingMessage(
752 message: string,
753 targetRect: DOMRect,
754 type: "success" | "error",
755 ) {
756 // Create floating message element
757 const floatingMsg = document.createElement("div");
758 floatingMsg.textContent = message;
759 floatingMsg.className = `floating-message ${type}`;
760
761 // Position it near the clicked element
762 // Position just above the element
763 const top = targetRect.top - 30;
764 const left = targetRect.left + targetRect.width / 2 - 40;
765
766 floatingMsg.style.position = "fixed";
767 floatingMsg.style.top = `${top}px`;
768 floatingMsg.style.left = `${left}px`;
769 floatingMsg.style.zIndex = "9999";
770
771 // Add to document body
772 document.body.appendChild(floatingMsg);
773
774 // Animate in
775 floatingMsg.style.opacity = "0";
776 floatingMsg.style.transform = "translateY(10px)";
777
778 setTimeout(() => {
779 floatingMsg.style.opacity = "1";
780 floatingMsg.style.transform = "translateY(0)";
781 }, 10);
782
783 // Remove after animation
784 setTimeout(() => {
785 floatingMsg.style.opacity = "0";
786 floatingMsg.style.transform = "translateY(-10px)";
787
788 setTimeout(() => {
789 document.body.removeChild(floatingMsg);
790 }, 300);
791 }, 1500);
792 }
793
philip.zeyliger6d3de482025-06-10 19:38:14 -0700794 // Format GitHub repository URL to org/repo format
795 formatGitHubRepo(url) {
796 if (!url) return null;
797
798 // Common GitHub URL patterns
799 const patterns = [
800 // HTTPS URLs
801 /https:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
802 // SSH URLs
803 /git@github\.com:([^/]+)\/([^/\s.]+)(?:\.git)?/,
804 // Git protocol
805 /git:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
806 ];
807
808 for (const pattern of patterns) {
809 const match = url.match(pattern);
810 if (match) {
811 return {
812 formatted: `${match[1]}/${match[2]}`,
813 url: `https://github.com/${match[1]}/${match[2]}`,
814 owner: match[1],
815 repo: match[2],
816 };
817 }
818 }
819
820 return null;
821 }
822
823 // Generate GitHub branch URL if linking is enabled
824 getGitHubBranchLink(branchName) {
825 if (!this.state?.link_to_github || !branchName) {
826 return null;
827 }
828
829 const github = this.formatGitHubRepo(this.state?.git_origin);
830 if (!github) {
831 return null;
832 }
833
834 return `https://github.com/${github.owner}/${github.repo}/tree/${branchName}`;
835 }
836
Sean McCullough86b56862025-04-18 13:04:03 -0700837 render() {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000838 // Calculate if this is an end of turn message with no parent conversation ID
839 const isEndOfTurn =
840 this.message?.end_of_turn && !this.message?.parent_conversation_id;
841
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700842 const isPreCompaction =
843 this.message?.idx !== undefined &&
844 this.message.idx < this.firstMessageIndex;
845
bankseanc5147482025-06-29 00:41:58 +0000846 // Dynamic classes based on message type and state
847 const messageClasses = [
848 "relative mb-1.5 flex flex-col w-full", // base message styles
849 isEndOfTurn ? "mb-4" : "", // end-of-turn spacing
850 isPreCompaction ? "opacity-85 border-l-2 border-gray-300" : "", // pre-compaction styling
851 ]
852 .filter(Boolean)
853 .join(" ");
854
855 const bubbleContainerClasses = [
856 "flex-1 flex overflow-hidden text-ellipsis",
857 this.compactPadding ? "max-w-full" : "max-w-[calc(100%-160px)]",
858 this.message?.type === "user" ? "justify-end" : "justify-start",
859 ]
860 .filter(Boolean)
861 .join(" ");
862
863 const messageContentClasses = [
864 "relative px-2.5 py-1.5 rounded-xl shadow-sm max-w-full w-fit min-w-min break-words word-break-words",
865 // User message styling
866 this.message?.type === "user"
867 ? "bg-blue-500 text-white rounded-br-sm"
868 : // Agent/tool/error message styling
869 "bg-gray-100 text-black rounded-bl-sm",
870 ]
871 .filter(Boolean)
872 .join(" ");
873
Sean McCullough86b56862025-04-18 13:04:03 -0700874 return html`
bankseanc5147482025-06-29 00:41:58 +0000875 <div class="${messageClasses}">
876 <div class="flex relative w-full">
877 <!-- Left metadata area -->
878 <div
879 class="${this.compactPadding
880 ? "hidden"
881 : "flex-none w-20 px-1 py-0.5 text-right text-xs text-gray-500 self-start"}"
882 ></div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000883
884 <!-- Message bubble -->
bankseanc5147482025-06-29 00:41:58 +0000885 <div class="${bubbleContainerClasses}">
886 <div class="${messageContentClasses}">
887 <div class="relative">
888 <div
889 class="absolute top-1 right-1 z-10 opacity-0 hover:opacity-100 transition-opacity duration-200 flex gap-1.5"
890 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000891 ${copyButton(this.message?.content)}
892 <button
bankseanc5147482025-06-29 00:41:58 +0000893 class="bg-transparent border-none ${this.message?.type ===
894 "user"
895 ? "text-white/80 hover:bg-white/15"
896 : "text-black/60 hover:bg-black/8"} cursor-pointer p-0.5 rounded-full flex items-center justify-center w-6 h-6 transition-all duration-150"
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000897 title="Show message details"
898 @click=${this._toggleInfo}
Sean McCullough71941bd2025-04-18 13:31:48 -0700899 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000900 <svg
901 xmlns="http://www.w3.org/2000/svg"
902 width="16"
903 height="16"
904 viewBox="0 0 24 24"
905 fill="none"
906 stroke="currentColor"
907 stroke-width="2"
908 stroke-linecap="round"
909 stroke-linejoin="round"
910 >
911 <circle cx="12" cy="12" r="10"></circle>
912 <line x1="12" y1="16" x2="12" y2="12"></line>
913 <line x1="12" y1="8" x2="12.01" y2="8"></line>
914 </svg>
915 </button>
916 </div>
917 ${this.message?.content
918 ? html`
bankseanc5147482025-06-29 00:41:58 +0000919 <div
920 class="overflow-x-auto mb-0 font-sans py-0.5 select-text cursor-text text-sm leading-relaxed text-left min-w-[200px] box-border mx-auto markdown-content"
921 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000922 ${unsafeHTML(
923 this.renderMarkdown(this.message?.content),
924 )}
925 </div>
926 `
927 : ""}
928
929 <!-- End of turn indicator inside the bubble -->
930 ${isEndOfTurn && this.message?.elapsed
931 ? html`
bankseanc5147482025-06-29 00:41:58 +0000932 <div
933 class="block text-xs ${this.message?.type === "user"
934 ? "text-white/70"
935 : "text-gray-500"} py-0.5 mt-2 text-right italic"
936 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000937 end of turn
938 (${this._formatDuration(this.message?.elapsed)})
939 </div>
940 `
941 : ""}
942
943 <!-- Info panel that can be toggled -->
944 ${this.showInfo
945 ? html`
bankseanc5147482025-06-29 00:41:58 +0000946 <div
947 class="mt-2 p-2 ${this.message?.type === "user"
948 ? "bg-white/15 border-l-2 border-white/20"
949 : "bg-black/5 border-l-2 border-black/10"} rounded-md text-xs transition-all duration-200"
950 >
951 <div class="mb-1 flex">
952 <span class="font-bold mr-1 min-w-[60px]">Type:</span>
953 <span class="flex-1">${this.message?.type}</span>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000954 </div>
bankseanc5147482025-06-29 00:41:58 +0000955 <div class="mb-1 flex">
956 <span class="font-bold mr-1 min-w-[60px]">Time:</span>
957 <span class="flex-1">
958 ${this.formatTimestamp(this.message?.timestamp, "")}
959 </span>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000960 </div>
961 ${this.message?.elapsed
962 ? html`
bankseanc5147482025-06-29 00:41:58 +0000963 <div class="mb-1 flex">
964 <span class="font-bold mr-1 min-w-[60px]"
965 >Duration:</span
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000966 >
bankseanc5147482025-06-29 00:41:58 +0000967 <span class="flex-1">
968 ${this._formatDuration(this.message?.elapsed)}
969 </span>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000970 </div>
971 `
972 : ""}
973 ${this.message?.usage
974 ? html`
bankseanc5147482025-06-29 00:41:58 +0000975 <div class="mb-1 flex">
976 <span class="font-bold mr-1 min-w-[60px]"
977 >Tokens:</span
978 >
979 <span class="flex-1">
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000980 ${this.message?.usage
981 ? html`
982 <div>
983 Input:
984 ${this.formatNumber(
985 this.message?.usage?.input_tokens ||
986 0,
987 )}
988 </div>
989 ${this.message?.usage
990 ?.cache_creation_input_tokens
991 ? html`
992 <div>
993 Cache creation:
994 ${this.formatNumber(
995 this.message?.usage
996 ?.cache_creation_input_tokens,
997 )}
998 </div>
999 `
1000 : ""}
1001 ${this.message?.usage
1002 ?.cache_read_input_tokens
1003 ? html`
1004 <div>
1005 Cache read:
1006 ${this.formatNumber(
1007 this.message?.usage
1008 ?.cache_read_input_tokens,
1009 )}
1010 </div>
1011 `
1012 : ""}
1013 <div>
1014 Output:
1015 ${this.formatNumber(
1016 this.message?.usage?.output_tokens,
1017 )}
1018 </div>
1019 <div>
1020 Cost:
1021 ${this.formatCurrency(
1022 this.message?.usage?.cost_usd,
1023 )}
1024 </div>
1025 `
1026 : "N/A"}
1027 </span>
1028 </div>
1029 `
1030 : ""}
1031 ${this.message?.conversation_id
1032 ? html`
bankseanc5147482025-06-29 00:41:58 +00001033 <div class="mb-1 flex">
1034 <span class="font-bold mr-1 min-w-[60px]"
1035 >Conversation ID:</span
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001036 >
bankseanc5147482025-06-29 00:41:58 +00001037 <span
1038 class="flex-1 font-mono text-xs break-all"
1039 >
1040 ${this.message?.conversation_id}
1041 </span>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001042 </div>
1043 `
1044 : ""}
1045 </div>
1046 `
1047 : ""}
1048 </div>
1049
1050 <!-- Tool calls - only shown for agent messages -->
1051 ${this.message?.type === "agent"
1052 ? html`
1053 <sketch-tool-calls
1054 .toolCalls=${this.message?.tool_calls}
1055 .open=${this.open}
1056 ></sketch-tool-calls>
1057 `
1058 : ""}
1059
bankseanc5147482025-06-29 00:41:58 +00001060 <!-- Commits section -->
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001061 ${this.message?.commits
1062 ? html`
bankseanc5147482025-06-29 00:41:58 +00001063 <div class="mt-2.5">
1064 <div
1065 class="bg-green-100 text-green-800 font-medium text-xs py-1.5 px-2.5 rounded-2xl mb-2 text-center shadow-sm"
1066 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001067 ${this.message.commits.length} new
1068 commit${this.message.commits.length > 1 ? "s" : ""}
1069 detected
1070 </div>
1071 ${this.message.commits.map((commit) => {
1072 return html`
bankseanc5147482025-06-29 00:41:58 +00001073 <div
Sean McCulloughfd67b012025-07-01 14:58:23 -07001074 class="text-sm bg-gray-100 rounded-lg overflow-hidden mb-1.5 shadow-sm p-1.5 px-2 flex items-center gap-2"
bankseanc5147482025-06-29 00:41:58 +00001075 >
Philip Zeyliger72682df2025-04-23 13:09:46 -07001076 <span
bankseanc5147482025-06-29 00:41:58 +00001077 class="text-blue-600 font-bold font-mono cursor-pointer no-underline bg-blue-600/10 py-0.5 px-1 rounded hover:bg-blue-600/20"
Philip Zeyliger72682df2025-04-23 13:09:46 -07001078 title="Click to copy: ${commit.hash}"
1079 @click=${(e) =>
1080 this.copyToClipboard(
1081 commit.hash.substring(0, 8),
1082 e,
1083 )}
1084 >
Pokey Rule7be879f2025-04-23 15:30:15 +01001085 ${commit.hash.substring(0, 8)}
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001086 </span>
1087 ${commit.pushed_branch
philip.zeyliger6d3de482025-06-10 19:38:14 -07001088 ? (() => {
1089 const githubLink = this.getGitHubBranchLink(
1090 commit.pushed_branch,
1091 );
1092 return html`
bankseanc5147482025-06-29 00:41:58 +00001093 <div class="flex items-center gap-1.5">
philip.zeyliger6d3de482025-06-10 19:38:14 -07001094 <span
bankseanc5147482025-06-29 00:41:58 +00001095 class="text-green-600 font-medium cursor-pointer font-mono bg-green-600/10 py-0.5 px-1 rounded hover:bg-green-600/20"
philip.zeyliger6d3de482025-06-10 19:38:14 -07001096 title="Click to copy: ${commit.pushed_branch}"
1097 @click=${(e) =>
1098 this.copyToClipboard(
1099 commit.pushed_branch,
1100 e,
1101 )}
1102 >${commit.pushed_branch}</span
1103 >
cbroebbdee42025-06-20 09:57:44 +00001104 <span
bankseanc5147482025-06-29 00:41:58 +00001105 class="opacity-70 flex items-center hover:opacity-100"
cbroebbdee42025-06-20 09:57:44 +00001106 @click=${(e) => {
1107 e.stopPropagation();
1108 this.copyToClipboard(
1109 commit.pushed_branch,
1110 e,
1111 );
1112 }}
1113 >
philip.zeyliger6d3de482025-06-10 19:38:14 -07001114 <svg
1115 xmlns="http://www.w3.org/2000/svg"
1116 width="14"
1117 height="14"
1118 viewBox="0 0 24 24"
1119 fill="none"
1120 stroke="currentColor"
1121 stroke-width="2"
1122 stroke-linecap="round"
1123 stroke-linejoin="round"
bankseanc5147482025-06-29 00:41:58 +00001124 class="align-middle"
philip.zeyliger6d3de482025-06-10 19:38:14 -07001125 >
1126 <rect
1127 x="9"
1128 y="9"
1129 width="13"
1130 height="13"
1131 rx="2"
1132 ry="2"
1133 ></rect>
1134 <path
1135 d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
1136 ></path>
1137 </svg>
1138 </span>
1139 ${githubLink
1140 ? html`
1141 <a
1142 href="${githubLink}"
1143 target="_blank"
1144 rel="noopener noreferrer"
bankseanc5147482025-06-29 00:41:58 +00001145 class="text-gray-600 no-underline flex items-center transition-colors duration-200 hover:text-blue-600"
philip.zeyliger6d3de482025-06-10 19:38:14 -07001146 title="Open ${commit.pushed_branch} on GitHub"
1147 @click=${(e) =>
1148 e.stopPropagation()}
1149 >
1150 <svg
bankseanc5147482025-06-29 00:41:58 +00001151 class="w-3.5 h-3.5"
philip.zeyliger6d3de482025-06-10 19:38:14 -07001152 viewBox="0 0 16 16"
1153 width="14"
1154 height="14"
1155 >
1156 <path
1157 fill="currentColor"
1158 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"
1159 />
1160 </svg>
1161 </a>
1162 `
1163 : ""}
1164 </div>
1165 `;
1166 })()
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001167 : ``}
bankseanc5147482025-06-29 00:41:58 +00001168 <span
1169 class="text-sm text-gray-700 flex-grow truncate"
Sean McCullough71941bd2025-04-18 13:31:48 -07001170 >
bankseanc5147482025-06-29 00:41:58 +00001171 ${commit.subject}
1172 </span>
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001173 <button
bankseanc5147482025-06-29 00:41:58 +00001174 class="py-0.5 px-2 border-0 rounded bg-blue-600 text-white text-xs cursor-pointer transition-all duration-200 block ml-auto hover:bg-blue-700"
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001175 @click=${() => this.showCommit(commit.hash)}
1176 >
1177 View Diff
1178 </button>
Sean McCullough86b56862025-04-18 13:04:03 -07001179 </div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001180 `;
1181 })}
1182 </div>
1183 `
1184 : ""}
1185 </div>
1186 </div>
1187
bankseanc5147482025-06-29 00:41:58 +00001188 <!-- Right metadata area -->
1189 <div
1190 class="${this.compactPadding
1191 ? "hidden"
1192 : "flex-none w-20 px-1 py-0.5 text-left text-xs text-gray-500 self-start"}"
1193 ></div>
Sean McCullough86b56862025-04-18 13:04:03 -07001194 </div>
bankseancad67b02025-06-27 21:57:05 +00001195
1196 <!-- User name for user messages - positioned outside and below the bubble -->
1197 ${this.message?.type === "user" && this.state?.git_username
1198 ? html`
bankseanc5147482025-06-29 00:41:58 +00001199 <div
1200 class="flex justify-end mt-1 ${this.compactPadding
1201 ? ""
1202 : "pr-20"}"
1203 >
1204 <div class="text-xs text-gray-600 italic text-right">
1205 ${this.state.git_username}
1206 </div>
bankseancad67b02025-06-27 21:57:05 +00001207 </div>
1208 `
1209 : ""}
Sean McCullough86b56862025-04-18 13:04:03 -07001210 </div>
1211 `;
1212 }
1213}
1214
Sean McCullough71941bd2025-04-18 13:31:48 -07001215function copyButton(textToCopy: string) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001216 // SVG for copy icon (two overlapping rectangles)
1217 const copyIcon = html`<svg
1218 xmlns="http://www.w3.org/2000/svg"
1219 width="16"
1220 height="16"
1221 viewBox="0 0 24 24"
1222 fill="none"
1223 stroke="currentColor"
1224 stroke-width="2"
1225 stroke-linecap="round"
1226 stroke-linejoin="round"
1227 >
1228 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1229 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1230 </svg>`;
1231
1232 // SVG for success check mark
1233 const successIcon = html`<svg
1234 xmlns="http://www.w3.org/2000/svg"
1235 width="16"
1236 height="16"
1237 viewBox="0 0 24 24"
1238 fill="none"
1239 stroke="currentColor"
1240 stroke-width="2"
1241 stroke-linecap="round"
1242 stroke-linejoin="round"
1243 >
1244 <path d="M20 6L9 17l-5-5"></path>
1245 </svg>`;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001246
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001247 const ret = html`<button
bankseanc5147482025-06-29 00:41:58 +00001248 class="bg-transparent border-none cursor-pointer p-0.5 rounded-full flex items-center justify-center w-6 h-6 transition-all duration-150"
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001249 title="Copy to clipboard"
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001250 @click=${(e: Event) => {
1251 e.stopPropagation();
1252 const copyButton = e.currentTarget as HTMLButtonElement;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001253 const originalInnerHTML = copyButton.innerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001254 navigator.clipboard
1255 .writeText(textToCopy)
1256 .then(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001257 copyButton.innerHTML = "";
1258 const successElement = document.createElement("div");
1259 copyButton.appendChild(successElement);
1260 render(successIcon, successElement);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001261 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001262 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001263 }, 2000);
1264 })
1265 .catch((err) => {
1266 console.error("Failed to copy text: ", err);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001267 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001268 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001269 }, 2000);
1270 });
1271 }}
1272 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001273 ${copyIcon}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001274 </button>`;
Sean McCullough86b56862025-04-18 13:04:03 -07001275
Sean McCullough71941bd2025-04-18 13:31:48 -07001276 return ret;
Sean McCullough86b56862025-04-18 13:04:03 -07001277}
1278
bankseanc5147482025-06-29 00:41:58 +00001279// Global styles are now injected in the component's connectedCallback() method
1280// to ensure they are added when the component is actually used, not at module load time
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001281
Sean McCullough86b56862025-04-18 13:04:03 -07001282declare global {
1283 interface HTMLElementTagNameMap {
1284 "sketch-timeline-message": SketchTimelineMessage;
1285 }
1286}