blob: a56911a2991a71cf68037415c3687163fcc87022 [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
Sean McCulloughf6e1dfe2025-07-03 14:59:40 -0700398 const containers = this.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
Sean McCulloughf6e1dfe2025-07-03 14:59:40 -0700456 const copyButtons = this.querySelectorAll(".code-copy-button");
Pokey Rulea10f1512025-05-15 13:53:26 +0000457 if (!copyButtons || copyButtons.length === 0) return;
458
459 // Add click event listener to each button
460 copyButtons.forEach((button) => {
461 button.addEventListener("click", (e) => {
462 e.stopPropagation();
463 const codeId = (button as HTMLElement).dataset.codeId;
464 if (!codeId) return;
465
Sean McCulloughf6e1dfe2025-07-03 14:59:40 -0700466 const codeElement = this.querySelector(`#${codeId}`);
Pokey Rulea10f1512025-05-15 13:53:26 +0000467 if (!codeElement) return;
468
469 const codeText = codeElement.textContent || "";
470 const buttonRect = button.getBoundingClientRect();
471
472 // Copy code to clipboard
473 navigator.clipboard
474 .writeText(codeText)
475 .then(() => {
476 // Show success indicator
477 const originalHTML = button.innerHTML;
478 button.innerHTML = `
479 <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">
480 <path d="M20 6L9 17l-5-5"></path>
481 </svg>
482 `;
483
484 // Display floating message
485 this.showFloatingMessage("Copied!", buttonRect, "success");
486
487 // Reset button after delay
488 setTimeout(() => {
489 button.innerHTML = originalHTML;
490 }, 2000);
491 })
492 .catch((err) => {
493 console.error("Failed to copy code:", err);
494 this.showFloatingMessage("Failed to copy!", buttonRect, "error");
495 });
496 });
497 });
498 }, 100); // Small delay to ensure DOM is ready
499 }
500
Sean McCullough86b56862025-04-18 13:04:03 -0700501 // See https://lit.dev/docs/components/lifecycle/
502 disconnectedCallback() {
503 super.disconnectedCallback();
504 }
505
506 renderMarkdown(markdownContent: string): string {
507 try {
Sean McCullough8d93e362025-04-27 23:32:18 +0000508 // Create a custom renderer
509 const renderer = new Renderer();
510 const originalCodeRenderer = renderer.code.bind(renderer);
Autoformatterdded2d62025-04-28 00:27:21 +0000511
Pokey Rulea10f1512025-05-15 13:53:26 +0000512 // Override the code renderer to handle mermaid diagrams and add copy buttons
Autoformatterdded2d62025-04-28 00:27:21 +0000513 renderer.code = function ({ text, lang, escaped }: Tokens.Code): string {
514 if (lang === "mermaid") {
Sean McCullough8d93e362025-04-27 23:32:18 +0000515 // Generate a unique ID for this diagram
516 const id = `mermaid-diagram-${Math.random().toString(36).substring(2, 10)}`;
Autoformatterdded2d62025-04-28 00:27:21 +0000517
Sean McCullough8d93e362025-04-27 23:32:18 +0000518 // Just create the container and mermaid div - we'll render it in the updated() lifecycle method
519 return `<div class="mermaid-container">
520 <div class="mermaid" id="${id}">${text}</div>
521 </div>`;
522 }
Pokey Rulea10f1512025-05-15 13:53:26 +0000523
Philip Zeyliger0d092842025-06-09 18:57:12 -0700524 // For regular code blocks, call the original renderer to get properly escaped HTML
525 const originalCodeHtml = originalCodeRenderer({ text, lang, escaped });
526
527 // Extract the code content from the original HTML to add our custom wrapper
528 // The original renderer returns: <pre><code class="language-x">escapedText</code></pre>
529 const codeMatch = originalCodeHtml.match(
530 /<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/,
531 );
532 if (!codeMatch) {
533 // Fallback to original if we can't parse it
534 return originalCodeHtml;
535 }
536
537 const escapedText = codeMatch[1];
Pokey Rulea10f1512025-05-15 13:53:26 +0000538 const id = `code-block-${Math.random().toString(36).substring(2, 10)}`;
539 const langClass = lang ? ` class="language-${lang}"` : "";
540
541 return `<div class="code-block-container">
542 <div class="code-block-header">
543 ${lang ? `<span class="code-language">${lang}</span>` : ""}
544 <button class="code-copy-button" title="Copy code" data-code-id="${id}">
545 <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">
546 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
547 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
548 </svg>
549 </button>
550 </div>
Philip Zeyliger0d092842025-06-09 18:57:12 -0700551 <pre><code id="${id}"${langClass}>${escapedText}</code></pre>
Pokey Rulea10f1512025-05-15 13:53:26 +0000552 </div>`;
Sean McCullough8d93e362025-04-27 23:32:18 +0000553 };
Autoformatterdded2d62025-04-28 00:27:21 +0000554
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000555 // Set markdown options for proper code block highlighting
Sean McCullough86b56862025-04-18 13:04:03 -0700556 const markedOptions: MarkedOptions = {
557 gfm: true, // GitHub Flavored Markdown
558 breaks: true, // Convert newlines to <br>
559 async: false,
Autoformatterdded2d62025-04-28 00:27:21 +0000560 renderer: renderer,
Sean McCullough86b56862025-04-18 13:04:03 -0700561 };
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000562
563 // Parse markdown and sanitize the output HTML with DOMPurify
564 const htmlOutput = marked.parse(markdownContent, markedOptions) as string;
565 return DOMPurify.sanitize(htmlOutput, {
566 // Allow common HTML elements that are safe
567 ALLOWED_TAGS: [
568 "p",
569 "br",
570 "strong",
571 "em",
572 "b",
573 "i",
574 "u",
575 "s",
576 "code",
577 "pre",
578 "h1",
579 "h2",
580 "h3",
581 "h4",
582 "h5",
583 "h6",
584 "ul",
585 "ol",
586 "li",
587 "blockquote",
588 "a",
589 "div",
590 "span", // For mermaid diagrams and code blocks
591 "svg",
592 "g",
593 "path",
594 "rect",
595 "circle",
596 "text",
597 "line",
598 "polygon", // For mermaid SVG
599 "button", // For code copy buttons
600 ],
601 ALLOWED_ATTR: [
602 "href",
603 "title",
604 "target",
605 "rel", // For links
606 "class",
607 "id", // For styling and functionality
608 "data-*", // For code copy buttons
609 // SVG attributes for mermaid diagrams
610 "viewBox",
611 "width",
612 "height",
613 "xmlns",
614 "fill",
615 "stroke",
616 "stroke-width",
617 "d",
618 "x",
619 "y",
620 "x1",
621 "y1",
622 "x2",
623 "y2",
624 "cx",
625 "cy",
626 "r",
627 "rx",
628 "ry",
629 "points",
630 "transform",
631 "text-anchor",
632 "font-size",
633 "font-family",
634 ],
635 // Allow data attributes for functionality
636 ALLOW_DATA_ATTR: true,
637 // Keep whitespace for code formatting
638 KEEP_CONTENT: true,
639 });
Sean McCullough86b56862025-04-18 13:04:03 -0700640 } catch (error) {
641 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000642 // Fallback to sanitized plain text if markdown parsing fails
643 return DOMPurify.sanitize(markdownContent);
Sean McCullough86b56862025-04-18 13:04:03 -0700644 }
645 }
646
647 /**
648 * Format timestamp for display
649 */
650 formatTimestamp(
651 timestamp: string | number | Date | null | undefined,
652 defaultValue: string = "",
653 ): string {
654 if (!timestamp) return defaultValue;
655 try {
656 const date = new Date(timestamp);
657 if (isNaN(date.getTime())) return defaultValue;
658
659 // Format: Mar 13, 2025 09:53:25 AM
660 return date.toLocaleString("en-US", {
661 month: "short",
662 day: "numeric",
663 year: "numeric",
664 hour: "numeric",
665 minute: "2-digit",
666 second: "2-digit",
667 hour12: true,
668 });
philip.zeyliger26bc6592025-06-30 20:15:30 -0700669 } catch {
Sean McCullough86b56862025-04-18 13:04:03 -0700670 return defaultValue;
671 }
672 }
673
674 formatNumber(
675 num: number | null | undefined,
676 defaultValue: string = "0",
677 ): string {
678 if (num === undefined || num === null) return defaultValue;
679 try {
680 return num.toLocaleString();
philip.zeyliger26bc6592025-06-30 20:15:30 -0700681 } catch {
Sean McCullough86b56862025-04-18 13:04:03 -0700682 return String(num);
683 }
684 }
685 formatCurrency(
686 num: number | string | null | undefined,
687 defaultValue: string = "$0.00",
688 isMessageLevel: boolean = false,
689 ): string {
690 if (num === undefined || num === null) return defaultValue;
691 try {
692 // Use 4 decimal places for message-level costs, 2 for totals
693 const decimalPlaces = isMessageLevel ? 4 : 2;
694 return `$${parseFloat(String(num)).toFixed(decimalPlaces)}`;
philip.zeyliger26bc6592025-06-30 20:15:30 -0700695 } catch {
Sean McCullough86b56862025-04-18 13:04:03 -0700696 return defaultValue;
697 }
698 }
699
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000700 // Format duration from nanoseconds to a human-readable string
701 _formatDuration(nanoseconds: number | null | undefined): string {
702 if (!nanoseconds) return "0s";
703
704 const seconds = nanoseconds / 1e9;
705
706 if (seconds < 60) {
707 return `${seconds.toFixed(1)}s`;
708 } else if (seconds < 3600) {
709 const minutes = Math.floor(seconds / 60);
710 const remainingSeconds = seconds % 60;
711 return `${minutes}min ${remainingSeconds.toFixed(0)}s`;
712 } else {
713 const hours = Math.floor(seconds / 3600);
714 const remainingSeconds = seconds % 3600;
715 const minutes = Math.floor(remainingSeconds / 60);
716 return `${hours}h ${minutes}min`;
717 }
718 }
719
Sean McCullough86b56862025-04-18 13:04:03 -0700720 showCommit(commitHash: string) {
Sean McCullough71941bd2025-04-18 13:31:48 -0700721 this.dispatchEvent(
722 new CustomEvent("show-commit-diff", {
723 bubbles: true,
724 composed: true,
725 detail: { commitHash },
726 }),
727 );
Sean McCullough86b56862025-04-18 13:04:03 -0700728 }
729
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000730 _toggleInfo(e: Event) {
731 e.stopPropagation();
732 this.showInfo = !this.showInfo;
733 }
734
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000735 copyToClipboard(text: string, event: Event) {
736 const element = event.currentTarget as HTMLElement;
737 const rect = element.getBoundingClientRect();
738
739 navigator.clipboard
740 .writeText(text)
741 .then(() => {
742 this.showFloatingMessage("Copied!", rect, "success");
743 })
744 .catch((err) => {
745 console.error("Failed to copy text: ", err);
746 this.showFloatingMessage("Failed to copy!", rect, "error");
747 });
748 }
749
750 showFloatingMessage(
751 message: string,
752 targetRect: DOMRect,
753 type: "success" | "error",
754 ) {
755 // Create floating message element
756 const floatingMsg = document.createElement("div");
757 floatingMsg.textContent = message;
758 floatingMsg.className = `floating-message ${type}`;
759
760 // Position it near the clicked element
761 // Position just above the element
762 const top = targetRect.top - 30;
763 const left = targetRect.left + targetRect.width / 2 - 40;
764
765 floatingMsg.style.position = "fixed";
766 floatingMsg.style.top = `${top}px`;
767 floatingMsg.style.left = `${left}px`;
768 floatingMsg.style.zIndex = "9999";
769
770 // Add to document body
771 document.body.appendChild(floatingMsg);
772
773 // Animate in
774 floatingMsg.style.opacity = "0";
775 floatingMsg.style.transform = "translateY(10px)";
776
777 setTimeout(() => {
778 floatingMsg.style.opacity = "1";
779 floatingMsg.style.transform = "translateY(0)";
780 }, 10);
781
782 // Remove after animation
783 setTimeout(() => {
784 floatingMsg.style.opacity = "0";
785 floatingMsg.style.transform = "translateY(-10px)";
786
787 setTimeout(() => {
788 document.body.removeChild(floatingMsg);
789 }, 300);
790 }, 1500);
791 }
792
philip.zeyliger6d3de482025-06-10 19:38:14 -0700793 // Format GitHub repository URL to org/repo format
794 formatGitHubRepo(url) {
795 if (!url) return null;
796
797 // Common GitHub URL patterns
798 const patterns = [
799 // HTTPS URLs
800 /https:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
801 // SSH URLs
802 /git@github\.com:([^/]+)\/([^/\s.]+)(?:\.git)?/,
803 // Git protocol
804 /git:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
805 ];
806
807 for (const pattern of patterns) {
808 const match = url.match(pattern);
809 if (match) {
810 return {
811 formatted: `${match[1]}/${match[2]}`,
812 url: `https://github.com/${match[1]}/${match[2]}`,
813 owner: match[1],
814 repo: match[2],
815 };
816 }
817 }
818
819 return null;
820 }
821
822 // Generate GitHub branch URL if linking is enabled
823 getGitHubBranchLink(branchName) {
824 if (!this.state?.link_to_github || !branchName) {
825 return null;
826 }
827
828 const github = this.formatGitHubRepo(this.state?.git_origin);
829 if (!github) {
830 return null;
831 }
832
833 return `https://github.com/${github.owner}/${github.repo}/tree/${branchName}`;
834 }
835
Sean McCullough86b56862025-04-18 13:04:03 -0700836 render() {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000837 // Calculate if this is an end of turn message with no parent conversation ID
838 const isEndOfTurn =
839 this.message?.end_of_turn && !this.message?.parent_conversation_id;
840
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700841 const isPreCompaction =
842 this.message?.idx !== undefined &&
843 this.message.idx < this.firstMessageIndex;
844
bankseanc5147482025-06-29 00:41:58 +0000845 // Dynamic classes based on message type and state
846 const messageClasses = [
847 "relative mb-1.5 flex flex-col w-full", // base message styles
848 isEndOfTurn ? "mb-4" : "", // end-of-turn spacing
849 isPreCompaction ? "opacity-85 border-l-2 border-gray-300" : "", // pre-compaction styling
850 ]
851 .filter(Boolean)
852 .join(" ");
853
854 const bubbleContainerClasses = [
855 "flex-1 flex overflow-hidden text-ellipsis",
856 this.compactPadding ? "max-w-full" : "max-w-[calc(100%-160px)]",
857 this.message?.type === "user" ? "justify-end" : "justify-start",
858 ]
859 .filter(Boolean)
860 .join(" ");
861
862 const messageContentClasses = [
863 "relative px-2.5 py-1.5 rounded-xl shadow-sm max-w-full w-fit min-w-min break-words word-break-words",
864 // User message styling
865 this.message?.type === "user"
866 ? "bg-blue-500 text-white rounded-br-sm"
867 : // Agent/tool/error message styling
868 "bg-gray-100 text-black rounded-bl-sm",
869 ]
870 .filter(Boolean)
871 .join(" ");
872
Sean McCullough86b56862025-04-18 13:04:03 -0700873 return html`
bankseanc5147482025-06-29 00:41:58 +0000874 <div class="${messageClasses}">
875 <div class="flex relative w-full">
876 <!-- Left metadata area -->
877 <div
878 class="${this.compactPadding
879 ? "hidden"
880 : "flex-none w-20 px-1 py-0.5 text-right text-xs text-gray-500 self-start"}"
881 ></div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000882
883 <!-- Message bubble -->
bankseanc5147482025-06-29 00:41:58 +0000884 <div class="${bubbleContainerClasses}">
885 <div class="${messageContentClasses}">
886 <div class="relative">
887 <div
888 class="absolute top-1 right-1 z-10 opacity-0 hover:opacity-100 transition-opacity duration-200 flex gap-1.5"
889 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000890 ${copyButton(this.message?.content)}
891 <button
bankseanc5147482025-06-29 00:41:58 +0000892 class="bg-transparent border-none ${this.message?.type ===
893 "user"
894 ? "text-white/80 hover:bg-white/15"
895 : "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 +0000896 title="Show message details"
897 @click=${this._toggleInfo}
Sean McCullough71941bd2025-04-18 13:31:48 -0700898 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000899 <svg
900 xmlns="http://www.w3.org/2000/svg"
901 width="16"
902 height="16"
903 viewBox="0 0 24 24"
904 fill="none"
905 stroke="currentColor"
906 stroke-width="2"
907 stroke-linecap="round"
908 stroke-linejoin="round"
909 >
910 <circle cx="12" cy="12" r="10"></circle>
911 <line x1="12" y1="16" x2="12" y2="12"></line>
912 <line x1="12" y1="8" x2="12.01" y2="8"></line>
913 </svg>
914 </button>
915 </div>
916 ${this.message?.content
917 ? html`
bankseanc5147482025-06-29 00:41:58 +0000918 <div
919 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"
920 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000921 ${unsafeHTML(
922 this.renderMarkdown(this.message?.content),
923 )}
924 </div>
925 `
926 : ""}
927
928 <!-- End of turn indicator inside the bubble -->
929 ${isEndOfTurn && this.message?.elapsed
930 ? html`
bankseanc5147482025-06-29 00:41:58 +0000931 <div
932 class="block text-xs ${this.message?.type === "user"
933 ? "text-white/70"
934 : "text-gray-500"} py-0.5 mt-2 text-right italic"
935 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000936 end of turn
937 (${this._formatDuration(this.message?.elapsed)})
938 </div>
939 `
940 : ""}
941
942 <!-- Info panel that can be toggled -->
943 ${this.showInfo
944 ? html`
bankseanc5147482025-06-29 00:41:58 +0000945 <div
946 class="mt-2 p-2 ${this.message?.type === "user"
947 ? "bg-white/15 border-l-2 border-white/20"
948 : "bg-black/5 border-l-2 border-black/10"} rounded-md text-xs transition-all duration-200"
949 >
950 <div class="mb-1 flex">
951 <span class="font-bold mr-1 min-w-[60px]">Type:</span>
952 <span class="flex-1">${this.message?.type}</span>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000953 </div>
bankseanc5147482025-06-29 00:41:58 +0000954 <div class="mb-1 flex">
955 <span class="font-bold mr-1 min-w-[60px]">Time:</span>
956 <span class="flex-1">
957 ${this.formatTimestamp(this.message?.timestamp, "")}
958 </span>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000959 </div>
960 ${this.message?.elapsed
961 ? html`
bankseanc5147482025-06-29 00:41:58 +0000962 <div class="mb-1 flex">
963 <span class="font-bold mr-1 min-w-[60px]"
964 >Duration:</span
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000965 >
bankseanc5147482025-06-29 00:41:58 +0000966 <span class="flex-1">
967 ${this._formatDuration(this.message?.elapsed)}
968 </span>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000969 </div>
970 `
971 : ""}
972 ${this.message?.usage
973 ? html`
bankseanc5147482025-06-29 00:41:58 +0000974 <div class="mb-1 flex">
975 <span class="font-bold mr-1 min-w-[60px]"
976 >Tokens:</span
977 >
978 <span class="flex-1">
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000979 ${this.message?.usage
980 ? html`
981 <div>
982 Input:
983 ${this.formatNumber(
984 this.message?.usage?.input_tokens ||
985 0,
986 )}
987 </div>
988 ${this.message?.usage
989 ?.cache_creation_input_tokens
990 ? html`
991 <div>
992 Cache creation:
993 ${this.formatNumber(
994 this.message?.usage
995 ?.cache_creation_input_tokens,
996 )}
997 </div>
998 `
999 : ""}
1000 ${this.message?.usage
1001 ?.cache_read_input_tokens
1002 ? html`
1003 <div>
1004 Cache read:
1005 ${this.formatNumber(
1006 this.message?.usage
1007 ?.cache_read_input_tokens,
1008 )}
1009 </div>
1010 `
1011 : ""}
1012 <div>
1013 Output:
1014 ${this.formatNumber(
1015 this.message?.usage?.output_tokens,
1016 )}
1017 </div>
1018 <div>
1019 Cost:
1020 ${this.formatCurrency(
1021 this.message?.usage?.cost_usd,
1022 )}
1023 </div>
1024 `
1025 : "N/A"}
1026 </span>
1027 </div>
1028 `
1029 : ""}
1030 ${this.message?.conversation_id
1031 ? html`
bankseanc5147482025-06-29 00:41:58 +00001032 <div class="mb-1 flex">
1033 <span class="font-bold mr-1 min-w-[60px]"
1034 >Conversation ID:</span
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001035 >
bankseanc5147482025-06-29 00:41:58 +00001036 <span
1037 class="flex-1 font-mono text-xs break-all"
1038 >
1039 ${this.message?.conversation_id}
1040 </span>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001041 </div>
1042 `
1043 : ""}
1044 </div>
1045 `
1046 : ""}
1047 </div>
1048
1049 <!-- Tool calls - only shown for agent messages -->
1050 ${this.message?.type === "agent"
1051 ? html`
1052 <sketch-tool-calls
1053 .toolCalls=${this.message?.tool_calls}
1054 .open=${this.open}
1055 ></sketch-tool-calls>
1056 `
1057 : ""}
1058
bankseanc5147482025-06-29 00:41:58 +00001059 <!-- Commits section -->
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001060 ${this.message?.commits
1061 ? html`
bankseanc5147482025-06-29 00:41:58 +00001062 <div class="mt-2.5">
1063 <div
1064 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"
1065 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001066 ${this.message.commits.length} new
1067 commit${this.message.commits.length > 1 ? "s" : ""}
1068 detected
1069 </div>
1070 ${this.message.commits.map((commit) => {
1071 return html`
bankseanc5147482025-06-29 00:41:58 +00001072 <div
Sean McCulloughfd67b012025-07-01 14:58:23 -07001073 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 +00001074 >
Philip Zeyliger72682df2025-04-23 13:09:46 -07001075 <span
bankseanc5147482025-06-29 00:41:58 +00001076 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 -07001077 title="Click to copy: ${commit.hash}"
1078 @click=${(e) =>
1079 this.copyToClipboard(
1080 commit.hash.substring(0, 8),
1081 e,
1082 )}
1083 >
Pokey Rule7be879f2025-04-23 15:30:15 +01001084 ${commit.hash.substring(0, 8)}
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001085 </span>
1086 ${commit.pushed_branch
philip.zeyliger6d3de482025-06-10 19:38:14 -07001087 ? (() => {
1088 const githubLink = this.getGitHubBranchLink(
1089 commit.pushed_branch,
1090 );
1091 return html`
bankseanc5147482025-06-29 00:41:58 +00001092 <div class="flex items-center gap-1.5">
philip.zeyliger6d3de482025-06-10 19:38:14 -07001093 <span
bankseanc5147482025-06-29 00:41:58 +00001094 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 -07001095 title="Click to copy: ${commit.pushed_branch}"
1096 @click=${(e) =>
1097 this.copyToClipboard(
1098 commit.pushed_branch,
1099 e,
1100 )}
1101 >${commit.pushed_branch}</span
1102 >
cbroebbdee42025-06-20 09:57:44 +00001103 <span
bankseanc5147482025-06-29 00:41:58 +00001104 class="opacity-70 flex items-center hover:opacity-100"
cbroebbdee42025-06-20 09:57:44 +00001105 @click=${(e) => {
1106 e.stopPropagation();
1107 this.copyToClipboard(
1108 commit.pushed_branch,
1109 e,
1110 );
1111 }}
1112 >
philip.zeyliger6d3de482025-06-10 19:38:14 -07001113 <svg
1114 xmlns="http://www.w3.org/2000/svg"
1115 width="14"
1116 height="14"
1117 viewBox="0 0 24 24"
1118 fill="none"
1119 stroke="currentColor"
1120 stroke-width="2"
1121 stroke-linecap="round"
1122 stroke-linejoin="round"
bankseanc5147482025-06-29 00:41:58 +00001123 class="align-middle"
philip.zeyliger6d3de482025-06-10 19:38:14 -07001124 >
1125 <rect
1126 x="9"
1127 y="9"
1128 width="13"
1129 height="13"
1130 rx="2"
1131 ry="2"
1132 ></rect>
1133 <path
1134 d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
1135 ></path>
1136 </svg>
1137 </span>
1138 ${githubLink
1139 ? html`
1140 <a
1141 href="${githubLink}"
1142 target="_blank"
1143 rel="noopener noreferrer"
bankseanc5147482025-06-29 00:41:58 +00001144 class="text-gray-600 no-underline flex items-center transition-colors duration-200 hover:text-blue-600"
philip.zeyliger6d3de482025-06-10 19:38:14 -07001145 title="Open ${commit.pushed_branch} on GitHub"
1146 @click=${(e) =>
1147 e.stopPropagation()}
1148 >
1149 <svg
bankseanc5147482025-06-29 00:41:58 +00001150 class="w-3.5 h-3.5"
philip.zeyliger6d3de482025-06-10 19:38:14 -07001151 viewBox="0 0 16 16"
1152 width="14"
1153 height="14"
1154 >
1155 <path
1156 fill="currentColor"
1157 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"
1158 />
1159 </svg>
1160 </a>
1161 `
1162 : ""}
1163 </div>
1164 `;
1165 })()
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001166 : ``}
bankseanc5147482025-06-29 00:41:58 +00001167 <span
1168 class="text-sm text-gray-700 flex-grow truncate"
Sean McCullough71941bd2025-04-18 13:31:48 -07001169 >
bankseanc5147482025-06-29 00:41:58 +00001170 ${commit.subject}
1171 </span>
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001172 <button
bankseanc5147482025-06-29 00:41:58 +00001173 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 +00001174 @click=${() => this.showCommit(commit.hash)}
1175 >
1176 View Diff
1177 </button>
Sean McCullough86b56862025-04-18 13:04:03 -07001178 </div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001179 `;
1180 })}
1181 </div>
1182 `
1183 : ""}
1184 </div>
1185 </div>
1186
bankseanc5147482025-06-29 00:41:58 +00001187 <!-- Right metadata area -->
1188 <div
1189 class="${this.compactPadding
1190 ? "hidden"
1191 : "flex-none w-20 px-1 py-0.5 text-left text-xs text-gray-500 self-start"}"
1192 ></div>
Sean McCullough86b56862025-04-18 13:04:03 -07001193 </div>
bankseancad67b02025-06-27 21:57:05 +00001194
1195 <!-- User name for user messages - positioned outside and below the bubble -->
1196 ${this.message?.type === "user" && this.state?.git_username
1197 ? html`
bankseanc5147482025-06-29 00:41:58 +00001198 <div
1199 class="flex justify-end mt-1 ${this.compactPadding
1200 ? ""
1201 : "pr-20"}"
1202 >
1203 <div class="text-xs text-gray-600 italic text-right">
1204 ${this.state.git_username}
1205 </div>
bankseancad67b02025-06-27 21:57:05 +00001206 </div>
1207 `
1208 : ""}
Sean McCullough86b56862025-04-18 13:04:03 -07001209 </div>
1210 `;
1211 }
1212}
1213
Sean McCullough71941bd2025-04-18 13:31:48 -07001214function copyButton(textToCopy: string) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001215 // SVG for copy icon (two overlapping rectangles)
1216 const copyIcon = html`<svg
1217 xmlns="http://www.w3.org/2000/svg"
1218 width="16"
1219 height="16"
1220 viewBox="0 0 24 24"
1221 fill="none"
1222 stroke="currentColor"
1223 stroke-width="2"
1224 stroke-linecap="round"
1225 stroke-linejoin="round"
1226 >
1227 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1228 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1229 </svg>`;
1230
1231 // SVG for success check mark
1232 const successIcon = html`<svg
1233 xmlns="http://www.w3.org/2000/svg"
1234 width="16"
1235 height="16"
1236 viewBox="0 0 24 24"
1237 fill="none"
1238 stroke="currentColor"
1239 stroke-width="2"
1240 stroke-linecap="round"
1241 stroke-linejoin="round"
1242 >
1243 <path d="M20 6L9 17l-5-5"></path>
1244 </svg>`;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001245
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001246 const ret = html`<button
bankseanc5147482025-06-29 00:41:58 +00001247 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 +00001248 title="Copy to clipboard"
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001249 @click=${(e: Event) => {
1250 e.stopPropagation();
1251 const copyButton = e.currentTarget as HTMLButtonElement;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001252 const originalInnerHTML = copyButton.innerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001253 navigator.clipboard
1254 .writeText(textToCopy)
1255 .then(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001256 copyButton.innerHTML = "";
1257 const successElement = document.createElement("div");
1258 copyButton.appendChild(successElement);
1259 render(successIcon, successElement);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001260 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001261 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001262 }, 2000);
1263 })
1264 .catch((err) => {
1265 console.error("Failed to copy text: ", err);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001266 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001267 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001268 }, 2000);
1269 });
1270 }}
1271 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001272 ${copyIcon}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001273 </button>`;
Sean McCullough86b56862025-04-18 13:04:03 -07001274
Sean McCullough71941bd2025-04-18 13:31:48 -07001275 return ret;
Sean McCullough86b56862025-04-18 13:04:03 -07001276}
1277
bankseanc5147482025-06-29 00:41:58 +00001278// Global styles are now injected in the component's connectedCallback() method
1279// to ensure they are added when the component is actually used, not at module load time
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001280
Sean McCullough86b56862025-04-18 13:04:03 -07001281declare global {
1282 interface HTMLElementTagNameMap {
1283 "sketch-timeline-message": SketchTimelineMessage;
1284 }
1285}