blob: b9c1455109d74744027ec4ef587b48c5c71b5694 [file] [log] [blame]
Sean McCullough86b56862025-04-18 13:04:03 -07001import { css, html, LitElement } from "lit";
2import { unsafeHTML } from "lit/directives/unsafe-html.js";
3import { customElement, property } from "lit/decorators.js";
Sean McCulloughd9f13372025-04-21 15:08:49 -07004import { AgentMessage } from "../types";
Sean McCullough8d93e362025-04-27 23:32:18 +00005import { marked, MarkedOptions, Renderer, Tokens } from "marked";
6import mermaid from "mermaid";
Sean McCullough86b56862025-04-18 13:04:03 -07007import "./sketch-tool-calls";
8@customElement("sketch-timeline-message")
9export class SketchTimelineMessage extends LitElement {
10 @property()
Sean McCulloughd9f13372025-04-21 15:08:49 -070011 message: AgentMessage;
Sean McCullough86b56862025-04-18 13:04:03 -070012
13 @property()
Sean McCulloughd9f13372025-04-21 15:08:49 -070014 previousMessage: AgentMessage;
Sean McCullough86b56862025-04-18 13:04:03 -070015
Sean McCullough2deac842025-04-21 18:17:57 -070016 @property()
17 open: boolean = false;
18
Sean McCullough86b56862025-04-18 13:04:03 -070019 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
20 // Note that these styles only apply to the scope of this web component's
21 // shadow DOM node, so they won't leak out or collide with CSS declared in
22 // other components or the containing web page (...unless you want it to do that).
23 static styles = css`
24 .message {
25 position: relative;
26 margin-bottom: 5px;
27 padding-left: 30px;
28 }
29
30 .message-icon {
31 position: absolute;
32 left: 10px;
33 top: 0;
34 transform: translateX(-50%);
35 width: 16px;
36 height: 16px;
37 border-radius: 3px;
38 text-align: center;
39 line-height: 16px;
40 color: #fff;
41 font-size: 10px;
42 }
43
44 .message-content {
45 position: relative;
46 padding: 5px 10px;
47 background: #fff;
48 border-radius: 3px;
49 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
50 border-left: 3px solid transparent;
51 }
52
53 /* Copy button styles */
54 .message-text-container,
55 .tool-result-container {
56 position: relative;
57 }
58
59 .message-actions {
60 position: absolute;
61 top: 5px;
62 right: 5px;
63 z-index: 10;
64 opacity: 0;
65 transition: opacity 0.2s ease;
66 }
67
68 .message-text-container:hover .message-actions,
69 .tool-result-container:hover .message-actions {
70 opacity: 1;
71 }
72
73 .copy-button {
74 background-color: rgba(255, 255, 255, 0.9);
75 border: 1px solid #ddd;
76 border-radius: 4px;
77 color: #555;
78 cursor: pointer;
79 font-size: 12px;
80 padding: 2px 8px;
81 transition: all 0.2s ease;
82 }
83
84 .copy-button:hover {
85 background-color: #f0f0f0;
86 color: #333;
87 }
88
89 /* Removed arrow decoration for a more compact look */
90
91 .message-header {
92 display: flex;
93 flex-wrap: wrap;
94 gap: 5px;
95 margin-bottom: 3px;
96 font-size: 12px;
97 }
98
99 .message-timestamp {
100 font-size: 10px;
101 color: #888;
102 font-style: italic;
103 margin-left: 3px;
104 }
105
106 .message-usage {
107 font-size: 10px;
108 color: #888;
109 margin-left: 3px;
110 }
111
112 .conversation-id {
113 font-family: monospace;
114 font-size: 12px;
115 padding: 2px 4px;
116 background-color: #f0f0f0;
117 border-radius: 3px;
118 margin-left: auto;
119 }
120
121 .parent-info {
122 font-size: 11px;
123 opacity: 0.8;
124 }
125
126 .subconversation {
127 border-left: 2px solid transparent;
128 padding-left: 5px;
129 margin-left: 20px;
130 transition: margin-left 0.3s ease;
131 }
132
133 .message-text {
134 overflow-x: auto;
135 margin-bottom: 3px;
136 font-family: monospace;
137 padding: 3px 5px;
138 background: rgb(236, 236, 236);
139 border-radius: 6px;
140 user-select: text;
141 cursor: text;
142 -webkit-user-select: text;
143 -moz-user-select: text;
144 -ms-user-select: text;
145 font-size: 13px;
146 line-height: 1.3;
147 }
148
149 .tool-details {
150 margin-top: 3px;
151 padding-top: 3px;
152 border-top: 1px dashed #e0e0e0;
153 font-size: 12px;
154 }
155
156 .tool-name {
157 font-size: 12px;
158 font-weight: bold;
159 margin-bottom: 2px;
160 background: #f0f0f0;
161 padding: 2px 4px;
162 border-radius: 2px;
163 display: flex;
164 align-items: center;
165 gap: 3px;
166 }
167
168 .tool-input,
169 .tool-result {
170 margin-top: 2px;
171 padding: 3px 5px;
172 background: #f7f7f7;
173 border-radius: 2px;
174 font-family: monospace;
175 font-size: 12px;
176 overflow-x: auto;
177 white-space: pre;
178 line-height: 1.3;
179 user-select: text;
180 cursor: text;
181 -webkit-user-select: text;
182 -moz-user-select: text;
183 -ms-user-select: text;
184 }
185
186 .tool-result {
187 max-height: 300px;
188 overflow-y: auto;
189 }
190
191 .usage-info {
192 margin-top: 10px;
193 padding-top: 10px;
194 border-top: 1px dashed #e0e0e0;
195 font-size: 12px;
196 color: #666;
197 }
198
199 /* Custom styles for IRC-like experience */
200 .user .message-content {
201 border-left-color: #2196f3;
202 }
203
204 .agent .message-content {
205 border-left-color: #4caf50;
206 }
207
208 .tool .message-content {
209 border-left-color: #ff9800;
210 }
211
212 .error .message-content {
213 border-left-color: #f44336;
214 }
215
216 /* Make message type display bold but without the IRC-style markers */
217 .message-type {
218 font-weight: bold;
219 }
220
221 /* Commit message styling */
222 .message.commit {
223 background-color: #f0f7ff;
224 border-left: 4px solid #0366d6;
225 }
226
227 .commits-container {
228 margin-top: 10px;
229 padding: 5px;
230 }
231
232 .commits-header {
233 font-weight: bold;
234 margin-bottom: 5px;
235 color: #24292e;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000236 display: flex;
237 justify-content: space-between;
238 align-items: center;
Sean McCullough86b56862025-04-18 13:04:03 -0700239 }
240
241 .commit-boxes-row {
242 display: flex;
243 flex-wrap: wrap;
244 gap: 8px;
245 margin-top: 8px;
246 }
247
248 .commit-box {
249 border: 1px solid #d1d5da;
250 border-radius: 4px;
251 overflow: hidden;
252 background-color: #ffffff;
253 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
254 max-width: 100%;
255 display: flex;
256 flex-direction: column;
257 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700258
Sean McCullough86b56862025-04-18 13:04:03 -0700259 .commit-preview {
260 padding: 8px 12px;
Sean McCullough86b56862025-04-18 13:04:03 -0700261 font-family: monospace;
262 background-color: #f6f8fa;
263 border-bottom: 1px dashed #d1d5da;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000264 display: flex;
265 align-items: center;
266 flex-wrap: wrap;
267 gap: 4px;
Sean McCullough86b56862025-04-18 13:04:03 -0700268 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700269
Sean McCullough86b56862025-04-18 13:04:03 -0700270 .commit-preview:hover {
271 background-color: #eef2f6;
272 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700273
Sean McCullough86b56862025-04-18 13:04:03 -0700274 .commit-hash {
275 color: #0366d6;
276 font-weight: bold;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000277 cursor: pointer;
278 margin-right: 8px;
279 text-decoration: none;
280 position: relative;
281 }
282
283 .commit-hash:hover {
284 text-decoration: underline;
285 }
286
287 .commit-hash:hover::after {
288 content: "📋";
289 font-size: 10px;
290 position: absolute;
291 top: -8px;
292 right: -12px;
293 opacity: 0.7;
294 }
295
296 .branch-wrapper {
297 margin-right: 8px;
298 color: #555;
299 }
300
301 .commit-branch {
302 color: #28a745;
303 font-weight: 500;
304 cursor: pointer;
305 text-decoration: none;
306 position: relative;
307 }
308
309 .commit-branch:hover {
310 text-decoration: underline;
311 }
312
313 .commit-branch:hover::after {
314 content: "📋";
315 font-size: 10px;
316 position: absolute;
317 top: -8px;
318 right: -12px;
319 opacity: 0.7;
320 }
321
322 .commit-preview {
323 display: flex;
324 align-items: center;
325 flex-wrap: wrap;
326 gap: 4px;
Sean McCullough86b56862025-04-18 13:04:03 -0700327 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700328
Sean McCullough86b56862025-04-18 13:04:03 -0700329 .commit-details {
330 padding: 8px 12px;
331 max-height: 200px;
332 overflow-y: auto;
333 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700334
Sean McCullough86b56862025-04-18 13:04:03 -0700335 .commit-details pre {
336 margin: 0;
337 white-space: pre-wrap;
338 word-break: break-word;
339 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700340
Sean McCullough86b56862025-04-18 13:04:03 -0700341 .commit-details.is-hidden {
342 display: none;
343 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700344
Sean McCullough86b56862025-04-18 13:04:03 -0700345 .pushed-branch {
346 color: #28a745;
347 font-weight: 500;
348 margin-left: 6px;
349 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700350
Sean McCullough86b56862025-04-18 13:04:03 -0700351 .commit-diff-button {
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000352 padding: 3px 6px;
Sean McCullough86b56862025-04-18 13:04:03 -0700353 border: 1px solid #ccc;
354 border-radius: 3px;
355 background-color: #f7f7f7;
356 color: #24292e;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000357 font-size: 11px;
Sean McCullough86b56862025-04-18 13:04:03 -0700358 cursor: pointer;
359 transition: all 0.2s ease;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000360 margin-left: auto;
Sean McCullough86b56862025-04-18 13:04:03 -0700361 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700362
Sean McCullough86b56862025-04-18 13:04:03 -0700363 .commit-diff-button:hover {
364 background-color: #e7e7e7;
365 border-color: #aaa;
366 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700367
Sean McCullough86b56862025-04-18 13:04:03 -0700368 /* Tool call cards */
369 .tool-call-cards-container {
370 display: flex;
371 flex-direction: column;
372 gap: 8px;
373 margin-top: 8px;
374 }
375
376 /* Message type styles */
377
378 .user .message-icon {
379 background-color: #2196f3;
380 }
381
382 .agent .message-icon {
383 background-color: #4caf50;
384 }
385
386 .tool .message-icon {
387 background-color: #ff9800;
388 }
389
390 .error .message-icon {
391 background-color: #f44336;
392 }
393
394 .end-of-turn {
395 margin-bottom: 15px;
396 }
397
398 .end-of-turn::after {
399 content: "End of Turn";
400 position: absolute;
401 left: 15px;
402 bottom: -10px;
403 transform: translateX(-50%);
404 font-size: 10px;
405 color: #666;
406 background: #f0f0f0;
407 padding: 1px 4px;
408 border-radius: 3px;
409 }
410
411 .markdown-content {
412 box-sizing: border-box;
413 min-width: 200px;
414 margin: 0 auto;
415 }
416
417 .markdown-content p {
418 margin-block-start: 0.5em;
419 margin-block-end: 0.5em;
420 }
Sean McCullough8d93e362025-04-27 23:32:18 +0000421
422 /* Mermaid diagram styling */
423 .mermaid-container {
424 margin: 1em 0;
425 padding: 0.5em;
426 background-color: #f8f8f8;
427 border-radius: 4px;
428 overflow-x: auto;
429 }
430
431 .mermaid {
432 text-align: center;
433 }
Sean McCullough86b56862025-04-18 13:04:03 -0700434 `;
435
Sean McCullough8d93e362025-04-27 23:32:18 +0000436 // Track mermaid diagrams that need rendering
437 private mermaidDiagrams = new Map();
438
Sean McCullough86b56862025-04-18 13:04:03 -0700439 constructor() {
440 super();
Sean McCullough8d93e362025-04-27 23:32:18 +0000441 // Initialize mermaid with specific config
442 mermaid.initialize({
443 startOnLoad: false,
444 theme: 'default',
445 securityLevel: 'loose', // Allows more flexibility but be careful with user-generated content
446 fontFamily: 'monospace'
447 });
Sean McCullough86b56862025-04-18 13:04:03 -0700448 }
449
450 // See https://lit.dev/docs/components/lifecycle/
451 connectedCallback() {
452 super.connectedCallback();
453 }
Sean McCullough8d93e362025-04-27 23:32:18 +0000454
455 // After the component is updated and rendered, render any mermaid diagrams
456 updated(changedProperties: Map<string, unknown>) {
457 super.updated(changedProperties);
458 this.renderMermaidDiagrams();
459 }
460
461 // Render mermaid diagrams after the component is updated
462 renderMermaidDiagrams() {
463 // Add a small delay to ensure the DOM is fully rendered
464 setTimeout(() => {
465 // Find all mermaid containers in our shadow root
466 const containers = this.shadowRoot?.querySelectorAll('.mermaid');
467 if (!containers || containers.length === 0) return;
468
469 // Process each mermaid diagram
470 containers.forEach(container => {
471 const id = container.id;
472 const code = container.textContent || '';
473 if (!code || !id) return; // Use return for forEach instead of continue
474
475 try {
476 // Clear any previous content
477 container.innerHTML = code;
478
479 // Render the mermaid diagram using promise
480 mermaid.render(`${id}-svg`, code)
481 .then(({ svg }) => {
482 container.innerHTML = svg;
483 })
484 .catch(err => {
485 console.error('Error rendering mermaid diagram:', err);
486 // Show the original code as fallback
487 container.innerHTML = `<pre>${code}</pre>`;
488 });
489 } catch (err) {
490 console.error('Error processing mermaid diagram:', err);
491 // Show the original code as fallback
492 container.innerHTML = `<pre>${code}</pre>`;
493 }
494 });
495 }, 100); // Small delay to ensure DOM is ready
496 }
Sean McCullough86b56862025-04-18 13:04:03 -0700497
498 // See https://lit.dev/docs/components/lifecycle/
499 disconnectedCallback() {
500 super.disconnectedCallback();
501 }
502
503 renderMarkdown(markdownContent: string): string {
504 try {
Sean McCullough8d93e362025-04-27 23:32:18 +0000505 // Create a custom renderer
506 const renderer = new Renderer();
507 const originalCodeRenderer = renderer.code.bind(renderer);
508
509 // Override the code renderer to handle mermaid diagrams
510 renderer.code = function({ text, lang, escaped }: Tokens.Code): string {
511 if (lang === 'mermaid') {
512 // Generate a unique ID for this diagram
513 const id = `mermaid-diagram-${Math.random().toString(36).substring(2, 10)}`;
514
515 // Just create the container and mermaid div - we'll render it in the updated() lifecycle method
516 return `<div class="mermaid-container">
517 <div class="mermaid" id="${id}">${text}</div>
518 </div>`;
519 }
520 // Default rendering for other code blocks
521 return originalCodeRenderer({ text, lang, escaped });
522 };
523
Sean McCullough86b56862025-04-18 13:04:03 -0700524 // Set markdown options for proper code block highlighting and safety
525 const markedOptions: MarkedOptions = {
526 gfm: true, // GitHub Flavored Markdown
527 breaks: true, // Convert newlines to <br>
528 async: false,
Sean McCullough8d93e362025-04-27 23:32:18 +0000529 renderer: renderer
Sean McCullough86b56862025-04-18 13:04:03 -0700530 // DOMPurify is recommended for production, but not included in this implementation
531 };
532 return marked.parse(markdownContent, markedOptions) as string;
533 } catch (error) {
534 console.error("Error rendering markdown:", error);
535 // Fallback to plain text if markdown parsing fails
536 return markdownContent;
537 }
538 }
539
540 /**
541 * Format timestamp for display
542 */
543 formatTimestamp(
544 timestamp: string | number | Date | null | undefined,
545 defaultValue: string = "",
546 ): string {
547 if (!timestamp) return defaultValue;
548 try {
549 const date = new Date(timestamp);
550 if (isNaN(date.getTime())) return defaultValue;
551
552 // Format: Mar 13, 2025 09:53:25 AM
553 return date.toLocaleString("en-US", {
554 month: "short",
555 day: "numeric",
556 year: "numeric",
557 hour: "numeric",
558 minute: "2-digit",
559 second: "2-digit",
560 hour12: true,
561 });
562 } catch (e) {
563 return defaultValue;
564 }
565 }
566
567 formatNumber(
568 num: number | null | undefined,
569 defaultValue: string = "0",
570 ): string {
571 if (num === undefined || num === null) return defaultValue;
572 try {
573 return num.toLocaleString();
574 } catch (e) {
575 return String(num);
576 }
577 }
578 formatCurrency(
579 num: number | string | null | undefined,
580 defaultValue: string = "$0.00",
581 isMessageLevel: boolean = false,
582 ): string {
583 if (num === undefined || num === null) return defaultValue;
584 try {
585 // Use 4 decimal places for message-level costs, 2 for totals
586 const decimalPlaces = isMessageLevel ? 4 : 2;
587 return `$${parseFloat(String(num)).toFixed(decimalPlaces)}`;
588 } catch (e) {
589 return defaultValue;
590 }
591 }
592
593 showCommit(commitHash: string) {
Sean McCullough71941bd2025-04-18 13:31:48 -0700594 this.dispatchEvent(
595 new CustomEvent("show-commit-diff", {
596 bubbles: true,
597 composed: true,
598 detail: { commitHash },
599 }),
600 );
Sean McCullough86b56862025-04-18 13:04:03 -0700601 }
602
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000603 copyToClipboard(text: string, event: Event) {
604 const element = event.currentTarget as HTMLElement;
605 const rect = element.getBoundingClientRect();
606
607 navigator.clipboard
608 .writeText(text)
609 .then(() => {
610 this.showFloatingMessage("Copied!", rect, "success");
611 })
612 .catch((err) => {
613 console.error("Failed to copy text: ", err);
614 this.showFloatingMessage("Failed to copy!", rect, "error");
615 });
616 }
617
618 showFloatingMessage(
619 message: string,
620 targetRect: DOMRect,
621 type: "success" | "error",
622 ) {
623 // Create floating message element
624 const floatingMsg = document.createElement("div");
625 floatingMsg.textContent = message;
626 floatingMsg.className = `floating-message ${type}`;
627
628 // Position it near the clicked element
629 // Position just above the element
630 const top = targetRect.top - 30;
631 const left = targetRect.left + targetRect.width / 2 - 40;
632
633 floatingMsg.style.position = "fixed";
634 floatingMsg.style.top = `${top}px`;
635 floatingMsg.style.left = `${left}px`;
636 floatingMsg.style.zIndex = "9999";
637
638 // Add to document body
639 document.body.appendChild(floatingMsg);
640
641 // Animate in
642 floatingMsg.style.opacity = "0";
643 floatingMsg.style.transform = "translateY(10px)";
644
645 setTimeout(() => {
646 floatingMsg.style.opacity = "1";
647 floatingMsg.style.transform = "translateY(0)";
648 }, 10);
649
650 // Remove after animation
651 setTimeout(() => {
652 floatingMsg.style.opacity = "0";
653 floatingMsg.style.transform = "translateY(-10px)";
654
655 setTimeout(() => {
656 document.body.removeChild(floatingMsg);
657 }, 300);
658 }, 1500);
659 }
660
Sean McCullough86b56862025-04-18 13:04:03 -0700661 render() {
662 return html`
663 <div
664 class="message ${this.message?.type} ${this.message?.end_of_turn
665 ? "end-of-turn"
666 : ""}"
667 >
668 ${this.previousMessage?.type != this.message?.type
669 ? html`<div class="message-icon">
670 ${this.message?.type.toUpperCase()[0]}
671 </div>`
672 : ""}
673 <div class="message-content">
674 <div class="message-header">
675 <span class="message-type">${this.message?.type}</span>
Sean McCullough71941bd2025-04-18 13:31:48 -0700676 <span class="message-timestamp"
677 >${this.formatTimestamp(this.message?.timestamp)}
678 ${this.message?.elapsed
679 ? html`(${(this.message?.elapsed / 1e9).toFixed(2)}s)`
680 : ""}</span
681 >
682 ${this.message?.usage
683 ? html` <span class="message-usage">
684 <span title="Input tokens"
Josh Bleecher Snyder35889972025-04-24 20:48:16 +0000685 >In:
Josh Bleecher Snydera0801ad2025-04-25 19:34:53 +0000686 ${this.formatNumber(
687 (this.message?.usage?.input_tokens || 0) +
Autoformatter5c70bfe2025-04-25 21:28:00 +0000688 (this.message?.usage?.cache_read_input_tokens || 0) +
689 (this.message?.usage?.cache_creation_input_tokens || 0),
Josh Bleecher Snydera0801ad2025-04-25 19:34:53 +0000690 )}</span
Sean McCullough71941bd2025-04-18 13:31:48 -0700691 >
Sean McCullough71941bd2025-04-18 13:31:48 -0700692 <span title="Output tokens"
Autoformatter5c70bfe2025-04-25 21:28:00 +0000693 >Out:
694 ${this.formatNumber(
695 this.message?.usage?.output_tokens,
696 )}</span
Sean McCullough71941bd2025-04-18 13:31:48 -0700697 >
698 <span title="Message cost"
699 >(${this.formatCurrency(
700 this.message?.usage?.cost_usd,
701 )})</span
702 >
703 </span>`
704 : ""}
Sean McCullough86b56862025-04-18 13:04:03 -0700705 </div>
706 <div class="message-text-container">
707 <div class="message-actions">
708 ${copyButton(this.message?.content)}
709 </div>
710 ${this.message?.content
711 ? html`
712 <div class="message-text markdown-content">
713 ${unsafeHTML(this.renderMarkdown(this.message?.content))}
714 </div>
715 `
716 : ""}
717 </div>
718 <sketch-tool-calls
719 .toolCalls=${this.message?.tool_calls}
Sean McCullough2deac842025-04-21 18:17:57 -0700720 .open=${this.open}
Sean McCullough86b56862025-04-18 13:04:03 -0700721 ></sketch-tool-calls>
722 ${this.message?.commits
723 ? html`
724 <div class="commits-container">
725 <div class="commits-header">
Sean McCullough71941bd2025-04-18 13:31:48 -0700726 ${this.message.commits.length} new
727 commit${this.message.commits.length > 1 ? "s" : ""} detected
Sean McCullough86b56862025-04-18 13:04:03 -0700728 </div>
729 ${this.message.commits.map((commit) => {
730 return html`
731 <div class="commit-boxes-row">
732 <div class="commit-box">
733 <div class="commit-preview">
Philip Zeyliger72682df2025-04-23 13:09:46 -0700734 <span
735 class="commit-hash"
736 title="Click to copy: ${commit.hash}"
737 @click=${(e) =>
738 this.copyToClipboard(
739 commit.hash.substring(0, 8),
740 e,
741 )}
742 >
Pokey Rule7be879f2025-04-23 15:30:15 +0100743 ${commit.hash.substring(0, 8)}
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000744 </span>
745 ${commit.pushed_branch
746 ? html`
Pokey Rule7be879f2025-04-23 15:30:15 +0100747 <span class="branch-wrapper">
748 (<span
749 class="commit-branch pushed-branch"
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000750 title="Click to copy: ${commit.pushed_branch}"
751 @click=${(e) =>
752 this.copyToClipboard(
753 commit.pushed_branch,
754 e,
755 )}
756 >${commit.pushed_branch}</span
Pokey Rule7be879f2025-04-23 15:30:15 +0100757 >)
758 </span>
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000759 `
760 : ``}
761 <span class="commit-subject"
762 >${commit.subject}</span
Sean McCullough71941bd2025-04-18 13:31:48 -0700763 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000764 <button
765 class="commit-diff-button"
766 @click=${() => this.showCommit(commit.hash)}
767 >
768 View Diff
769 </button>
Sean McCullough86b56862025-04-18 13:04:03 -0700770 </div>
771 <div class="commit-details is-hidden">
772 <pre>${commit.body}</pre>
773 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700774 </div>
775 </div>
776 `;
777 })}
778 </div>
779 `
780 : ""}
781 </div>
782 </div>
783 `;
784 }
785}
786
Sean McCullough71941bd2025-04-18 13:31:48 -0700787function copyButton(textToCopy: string) {
Sean McCullough86b56862025-04-18 13:04:03 -0700788 // Add click event listener to handle copying
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000789 const buttonClass = "copy-button";
790 const buttonContent = "Copy";
791 const successContent = "Copied!";
792 const failureContent = "Failed";
793
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700794 const ret = html`<button
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000795 class="${buttonClass}"
796 title="Copy to clipboard"
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700797 @click=${(e: Event) => {
798 e.stopPropagation();
799 const copyButton = e.currentTarget as HTMLButtonElement;
800 navigator.clipboard
801 .writeText(textToCopy)
802 .then(() => {
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000803 copyButton.textContent = successContent;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700804 setTimeout(() => {
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000805 copyButton.textContent = buttonContent;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700806 }, 2000);
807 })
808 .catch((err) => {
809 console.error("Failed to copy text: ", err);
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000810 copyButton.textContent = failureContent;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700811 setTimeout(() => {
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000812 copyButton.textContent = buttonContent;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700813 }, 2000);
814 });
815 }}
816 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000817 ${buttonContent}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700818 </button>`;
Sean McCullough86b56862025-04-18 13:04:03 -0700819
Sean McCullough71941bd2025-04-18 13:31:48 -0700820 return ret;
Sean McCullough86b56862025-04-18 13:04:03 -0700821}
822
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000823// Create global styles for floating messages
824const floatingMessageStyles = document.createElement("style");
825floatingMessageStyles.textContent = `
826 .floating-message {
827 background-color: rgba(0, 0, 0, 0.8);
828 color: white;
829 padding: 5px 10px;
830 border-radius: 4px;
831 font-size: 12px;
832 font-family: system-ui, sans-serif;
833 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
834 pointer-events: none;
835 transition: opacity 0.3s ease, transform 0.3s ease;
836 }
837
838 .floating-message.success {
839 background-color: rgba(40, 167, 69, 0.9);
840 }
841
842 .floating-message.error {
843 background-color: rgba(220, 53, 69, 0.9);
844 }
845`;
846document.head.appendChild(floatingMessageStyles);
847
Sean McCullough86b56862025-04-18 13:04:03 -0700848declare global {
849 interface HTMLElementTagNameMap {
850 "sketch-timeline-message": SketchTimelineMessage;
851 }
852}