blob: 1e950d5967edadc230b073e102f379831b95a88a [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 }
Autoformatterdded2d62025-04-28 00:27:21 +0000421
Sean McCullough8d93e362025-04-27 23:32:18 +0000422 /* 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 }
Autoformatterdded2d62025-04-28 00:27:21 +0000430
Sean McCullough8d93e362025-04-27 23:32:18 +0000431 .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,
Autoformatterdded2d62025-04-28 00:27:21 +0000444 theme: "default",
445 securityLevel: "loose", // Allows more flexibility but be careful with user-generated content
446 fontFamily: "monospace",
Sean McCullough8d93e362025-04-27 23:32:18 +0000447 });
Sean McCullough86b56862025-04-18 13:04:03 -0700448 }
449
450 // See https://lit.dev/docs/components/lifecycle/
451 connectedCallback() {
452 super.connectedCallback();
453 }
Autoformatterdded2d62025-04-28 00:27:21 +0000454
Sean McCullough8d93e362025-04-27 23:32:18 +0000455 // 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 }
Autoformatterdded2d62025-04-28 00:27:21 +0000460
Sean McCullough8d93e362025-04-27 23:32:18 +0000461 // 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
Autoformatterdded2d62025-04-28 00:27:21 +0000466 const containers = this.shadowRoot?.querySelectorAll(".mermaid");
Sean McCullough8d93e362025-04-27 23:32:18 +0000467 if (!containers || containers.length === 0) return;
Autoformatterdded2d62025-04-28 00:27:21 +0000468
Sean McCullough8d93e362025-04-27 23:32:18 +0000469 // Process each mermaid diagram
Autoformatterdded2d62025-04-28 00:27:21 +0000470 containers.forEach((container) => {
Sean McCullough8d93e362025-04-27 23:32:18 +0000471 const id = container.id;
Autoformatterdded2d62025-04-28 00:27:21 +0000472 const code = container.textContent || "";
Sean McCullough8d93e362025-04-27 23:32:18 +0000473 if (!code || !id) return; // Use return for forEach instead of continue
Autoformatterdded2d62025-04-28 00:27:21 +0000474
Sean McCullough8d93e362025-04-27 23:32:18 +0000475 try {
476 // Clear any previous content
477 container.innerHTML = code;
Autoformatterdded2d62025-04-28 00:27:21 +0000478
Sean McCullough8d93e362025-04-27 23:32:18 +0000479 // Render the mermaid diagram using promise
Autoformatterdded2d62025-04-28 00:27:21 +0000480 mermaid
481 .render(`${id}-svg`, code)
Sean McCullough8d93e362025-04-27 23:32:18 +0000482 .then(({ svg }) => {
483 container.innerHTML = svg;
484 })
Autoformatterdded2d62025-04-28 00:27:21 +0000485 .catch((err) => {
486 console.error("Error rendering mermaid diagram:", err);
Sean McCullough8d93e362025-04-27 23:32:18 +0000487 // Show the original code as fallback
488 container.innerHTML = `<pre>${code}</pre>`;
489 });
490 } catch (err) {
Autoformatterdded2d62025-04-28 00:27:21 +0000491 console.error("Error processing mermaid diagram:", err);
Sean McCullough8d93e362025-04-27 23:32:18 +0000492 // Show the original code as fallback
493 container.innerHTML = `<pre>${code}</pre>`;
494 }
495 });
496 }, 100); // Small delay to ensure DOM is ready
497 }
Sean McCullough86b56862025-04-18 13:04:03 -0700498
499 // See https://lit.dev/docs/components/lifecycle/
500 disconnectedCallback() {
501 super.disconnectedCallback();
502 }
503
504 renderMarkdown(markdownContent: string): string {
505 try {
Sean McCullough8d93e362025-04-27 23:32:18 +0000506 // Create a custom renderer
507 const renderer = new Renderer();
508 const originalCodeRenderer = renderer.code.bind(renderer);
Autoformatterdded2d62025-04-28 00:27:21 +0000509
Sean McCullough8d93e362025-04-27 23:32:18 +0000510 // Override the code renderer to handle mermaid diagrams
Autoformatterdded2d62025-04-28 00:27:21 +0000511 renderer.code = function ({ text, lang, escaped }: Tokens.Code): string {
512 if (lang === "mermaid") {
Sean McCullough8d93e362025-04-27 23:32:18 +0000513 // Generate a unique ID for this diagram
514 const id = `mermaid-diagram-${Math.random().toString(36).substring(2, 10)}`;
Autoformatterdded2d62025-04-28 00:27:21 +0000515
Sean McCullough8d93e362025-04-27 23:32:18 +0000516 // Just create the container and mermaid div - we'll render it in the updated() lifecycle method
517 return `<div class="mermaid-container">
518 <div class="mermaid" id="${id}">${text}</div>
519 </div>`;
520 }
521 // Default rendering for other code blocks
522 return originalCodeRenderer({ text, lang, escaped });
523 };
Autoformatterdded2d62025-04-28 00:27:21 +0000524
Sean McCullough86b56862025-04-18 13:04:03 -0700525 // Set markdown options for proper code block highlighting and safety
526 const markedOptions: MarkedOptions = {
527 gfm: true, // GitHub Flavored Markdown
528 breaks: true, // Convert newlines to <br>
529 async: false,
Autoformatterdded2d62025-04-28 00:27:21 +0000530 renderer: renderer,
Sean McCullough86b56862025-04-18 13:04:03 -0700531 // DOMPurify is recommended for production, but not included in this implementation
532 };
533 return marked.parse(markdownContent, markedOptions) as string;
534 } catch (error) {
535 console.error("Error rendering markdown:", error);
536 // Fallback to plain text if markdown parsing fails
537 return markdownContent;
538 }
539 }
540
541 /**
542 * Format timestamp for display
543 */
544 formatTimestamp(
545 timestamp: string | number | Date | null | undefined,
546 defaultValue: string = "",
547 ): string {
548 if (!timestamp) return defaultValue;
549 try {
550 const date = new Date(timestamp);
551 if (isNaN(date.getTime())) return defaultValue;
552
553 // Format: Mar 13, 2025 09:53:25 AM
554 return date.toLocaleString("en-US", {
555 month: "short",
556 day: "numeric",
557 year: "numeric",
558 hour: "numeric",
559 minute: "2-digit",
560 second: "2-digit",
561 hour12: true,
562 });
563 } catch (e) {
564 return defaultValue;
565 }
566 }
567
568 formatNumber(
569 num: number | null | undefined,
570 defaultValue: string = "0",
571 ): string {
572 if (num === undefined || num === null) return defaultValue;
573 try {
574 return num.toLocaleString();
575 } catch (e) {
576 return String(num);
577 }
578 }
579 formatCurrency(
580 num: number | string | null | undefined,
581 defaultValue: string = "$0.00",
582 isMessageLevel: boolean = false,
583 ): string {
584 if (num === undefined || num === null) return defaultValue;
585 try {
586 // Use 4 decimal places for message-level costs, 2 for totals
587 const decimalPlaces = isMessageLevel ? 4 : 2;
588 return `$${parseFloat(String(num)).toFixed(decimalPlaces)}`;
589 } catch (e) {
590 return defaultValue;
591 }
592 }
593
594 showCommit(commitHash: string) {
Sean McCullough71941bd2025-04-18 13:31:48 -0700595 this.dispatchEvent(
596 new CustomEvent("show-commit-diff", {
597 bubbles: true,
598 composed: true,
599 detail: { commitHash },
600 }),
601 );
Sean McCullough86b56862025-04-18 13:04:03 -0700602 }
603
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000604 copyToClipboard(text: string, event: Event) {
605 const element = event.currentTarget as HTMLElement;
606 const rect = element.getBoundingClientRect();
607
608 navigator.clipboard
609 .writeText(text)
610 .then(() => {
611 this.showFloatingMessage("Copied!", rect, "success");
612 })
613 .catch((err) => {
614 console.error("Failed to copy text: ", err);
615 this.showFloatingMessage("Failed to copy!", rect, "error");
616 });
617 }
618
619 showFloatingMessage(
620 message: string,
621 targetRect: DOMRect,
622 type: "success" | "error",
623 ) {
624 // Create floating message element
625 const floatingMsg = document.createElement("div");
626 floatingMsg.textContent = message;
627 floatingMsg.className = `floating-message ${type}`;
628
629 // Position it near the clicked element
630 // Position just above the element
631 const top = targetRect.top - 30;
632 const left = targetRect.left + targetRect.width / 2 - 40;
633
634 floatingMsg.style.position = "fixed";
635 floatingMsg.style.top = `${top}px`;
636 floatingMsg.style.left = `${left}px`;
637 floatingMsg.style.zIndex = "9999";
638
639 // Add to document body
640 document.body.appendChild(floatingMsg);
641
642 // Animate in
643 floatingMsg.style.opacity = "0";
644 floatingMsg.style.transform = "translateY(10px)";
645
646 setTimeout(() => {
647 floatingMsg.style.opacity = "1";
648 floatingMsg.style.transform = "translateY(0)";
649 }, 10);
650
651 // Remove after animation
652 setTimeout(() => {
653 floatingMsg.style.opacity = "0";
654 floatingMsg.style.transform = "translateY(-10px)";
655
656 setTimeout(() => {
657 document.body.removeChild(floatingMsg);
658 }, 300);
659 }, 1500);
660 }
661
Sean McCullough86b56862025-04-18 13:04:03 -0700662 render() {
663 return html`
664 <div
665 class="message ${this.message?.type} ${this.message?.end_of_turn
666 ? "end-of-turn"
667 : ""}"
668 >
669 ${this.previousMessage?.type != this.message?.type
670 ? html`<div class="message-icon">
671 ${this.message?.type.toUpperCase()[0]}
672 </div>`
673 : ""}
674 <div class="message-content">
675 <div class="message-header">
676 <span class="message-type">${this.message?.type}</span>
Sean McCullough71941bd2025-04-18 13:31:48 -0700677 <span class="message-timestamp"
678 >${this.formatTimestamp(this.message?.timestamp)}
679 ${this.message?.elapsed
680 ? html`(${(this.message?.elapsed / 1e9).toFixed(2)}s)`
681 : ""}</span
682 >
683 ${this.message?.usage
684 ? html` <span class="message-usage">
685 <span title="Input tokens"
Josh Bleecher Snyder35889972025-04-24 20:48:16 +0000686 >In:
Josh Bleecher Snydera0801ad2025-04-25 19:34:53 +0000687 ${this.formatNumber(
688 (this.message?.usage?.input_tokens || 0) +
Autoformatter5c70bfe2025-04-25 21:28:00 +0000689 (this.message?.usage?.cache_read_input_tokens || 0) +
690 (this.message?.usage?.cache_creation_input_tokens || 0),
Josh Bleecher Snydera0801ad2025-04-25 19:34:53 +0000691 )}</span
Sean McCullough71941bd2025-04-18 13:31:48 -0700692 >
Sean McCullough71941bd2025-04-18 13:31:48 -0700693 <span title="Output tokens"
Autoformatter5c70bfe2025-04-25 21:28:00 +0000694 >Out:
695 ${this.formatNumber(
696 this.message?.usage?.output_tokens,
697 )}</span
Sean McCullough71941bd2025-04-18 13:31:48 -0700698 >
699 <span title="Message cost"
700 >(${this.formatCurrency(
701 this.message?.usage?.cost_usd,
702 )})</span
703 >
704 </span>`
705 : ""}
Sean McCullough86b56862025-04-18 13:04:03 -0700706 </div>
707 <div class="message-text-container">
708 <div class="message-actions">
709 ${copyButton(this.message?.content)}
710 </div>
711 ${this.message?.content
712 ? html`
713 <div class="message-text markdown-content">
714 ${unsafeHTML(this.renderMarkdown(this.message?.content))}
715 </div>
716 `
717 : ""}
718 </div>
719 <sketch-tool-calls
720 .toolCalls=${this.message?.tool_calls}
Sean McCullough2deac842025-04-21 18:17:57 -0700721 .open=${this.open}
Sean McCullough86b56862025-04-18 13:04:03 -0700722 ></sketch-tool-calls>
723 ${this.message?.commits
724 ? html`
725 <div class="commits-container">
726 <div class="commits-header">
Sean McCullough71941bd2025-04-18 13:31:48 -0700727 ${this.message.commits.length} new
728 commit${this.message.commits.length > 1 ? "s" : ""} detected
Sean McCullough86b56862025-04-18 13:04:03 -0700729 </div>
730 ${this.message.commits.map((commit) => {
731 return html`
732 <div class="commit-boxes-row">
733 <div class="commit-box">
734 <div class="commit-preview">
Philip Zeyliger72682df2025-04-23 13:09:46 -0700735 <span
736 class="commit-hash"
737 title="Click to copy: ${commit.hash}"
738 @click=${(e) =>
739 this.copyToClipboard(
740 commit.hash.substring(0, 8),
741 e,
742 )}
743 >
Pokey Rule7be879f2025-04-23 15:30:15 +0100744 ${commit.hash.substring(0, 8)}
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000745 </span>
746 ${commit.pushed_branch
747 ? html`
Pokey Rule7be879f2025-04-23 15:30:15 +0100748 <span class="branch-wrapper">
749 (<span
750 class="commit-branch pushed-branch"
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000751 title="Click to copy: ${commit.pushed_branch}"
752 @click=${(e) =>
753 this.copyToClipboard(
754 commit.pushed_branch,
755 e,
756 )}
757 >${commit.pushed_branch}</span
Pokey Rule7be879f2025-04-23 15:30:15 +0100758 >)
759 </span>
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000760 `
761 : ``}
762 <span class="commit-subject"
763 >${commit.subject}</span
Sean McCullough71941bd2025-04-18 13:31:48 -0700764 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000765 <button
766 class="commit-diff-button"
767 @click=${() => this.showCommit(commit.hash)}
768 >
769 View Diff
770 </button>
Sean McCullough86b56862025-04-18 13:04:03 -0700771 </div>
772 <div class="commit-details is-hidden">
773 <pre>${commit.body}</pre>
774 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700775 </div>
776 </div>
777 `;
778 })}
779 </div>
780 `
781 : ""}
782 </div>
783 </div>
784 `;
785 }
786}
787
Sean McCullough71941bd2025-04-18 13:31:48 -0700788function copyButton(textToCopy: string) {
Sean McCullough86b56862025-04-18 13:04:03 -0700789 // Add click event listener to handle copying
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000790 const buttonClass = "copy-button";
791 const buttonContent = "Copy";
792 const successContent = "Copied!";
793 const failureContent = "Failed";
794
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700795 const ret = html`<button
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000796 class="${buttonClass}"
797 title="Copy to clipboard"
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700798 @click=${(e: Event) => {
799 e.stopPropagation();
800 const copyButton = e.currentTarget as HTMLButtonElement;
801 navigator.clipboard
802 .writeText(textToCopy)
803 .then(() => {
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000804 copyButton.textContent = successContent;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700805 setTimeout(() => {
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000806 copyButton.textContent = buttonContent;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700807 }, 2000);
808 })
809 .catch((err) => {
810 console.error("Failed to copy text: ", err);
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000811 copyButton.textContent = failureContent;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700812 setTimeout(() => {
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000813 copyButton.textContent = buttonContent;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700814 }, 2000);
815 });
816 }}
817 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000818 ${buttonContent}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700819 </button>`;
Sean McCullough86b56862025-04-18 13:04:03 -0700820
Sean McCullough71941bd2025-04-18 13:31:48 -0700821 return ret;
Sean McCullough86b56862025-04-18 13:04:03 -0700822}
823
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000824// Create global styles for floating messages
825const floatingMessageStyles = document.createElement("style");
826floatingMessageStyles.textContent = `
827 .floating-message {
828 background-color: rgba(0, 0, 0, 0.8);
829 color: white;
830 padding: 5px 10px;
831 border-radius: 4px;
832 font-size: 12px;
833 font-family: system-ui, sans-serif;
834 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
835 pointer-events: none;
836 transition: opacity 0.3s ease, transform 0.3s ease;
837 }
838
839 .floating-message.success {
840 background-color: rgba(40, 167, 69, 0.9);
841 }
842
843 .floating-message.error {
844 background-color: rgba(220, 53, 69, 0.9);
845 }
846`;
847document.head.appendChild(floatingMessageStyles);
848
Sean McCullough86b56862025-04-18 13:04:03 -0700849declare global {
850 interface HTMLElementTagNameMap {
851 "sketch-timeline-message": SketchTimelineMessage;
852 }
853}