blob: 118fe366c2b9ddf2d8f812bade773a4ab2cd3f90 [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,
Sean McCulloughf98d7302025-04-27 17:44:06 -0700444 suppressErrorRendering: true,
Autoformatterdded2d62025-04-28 00:27:21 +0000445 theme: "default",
446 securityLevel: "loose", // Allows more flexibility but be careful with user-generated content
447 fontFamily: "monospace",
Sean McCullough8d93e362025-04-27 23:32:18 +0000448 });
Sean McCullough86b56862025-04-18 13:04:03 -0700449 }
450
451 // See https://lit.dev/docs/components/lifecycle/
452 connectedCallback() {
453 super.connectedCallback();
454 }
Autoformatterdded2d62025-04-28 00:27:21 +0000455
Sean McCullough8d93e362025-04-27 23:32:18 +0000456 // After the component is updated and rendered, render any mermaid diagrams
457 updated(changedProperties: Map<string, unknown>) {
458 super.updated(changedProperties);
459 this.renderMermaidDiagrams();
460 }
Autoformatterdded2d62025-04-28 00:27:21 +0000461
Sean McCullough8d93e362025-04-27 23:32:18 +0000462 // Render mermaid diagrams after the component is updated
463 renderMermaidDiagrams() {
464 // Add a small delay to ensure the DOM is fully rendered
465 setTimeout(() => {
466 // Find all mermaid containers in our shadow root
Autoformatterdded2d62025-04-28 00:27:21 +0000467 const containers = this.shadowRoot?.querySelectorAll(".mermaid");
Sean McCullough8d93e362025-04-27 23:32:18 +0000468 if (!containers || containers.length === 0) return;
Autoformatterdded2d62025-04-28 00:27:21 +0000469
Sean McCullough8d93e362025-04-27 23:32:18 +0000470 // Process each mermaid diagram
Autoformatterdded2d62025-04-28 00:27:21 +0000471 containers.forEach((container) => {
Sean McCullough8d93e362025-04-27 23:32:18 +0000472 const id = container.id;
Autoformatterdded2d62025-04-28 00:27:21 +0000473 const code = container.textContent || "";
Sean McCullough8d93e362025-04-27 23:32:18 +0000474 if (!code || !id) return; // Use return for forEach instead of continue
Autoformatterdded2d62025-04-28 00:27:21 +0000475
Sean McCullough8d93e362025-04-27 23:32:18 +0000476 try {
477 // Clear any previous content
478 container.innerHTML = code;
Autoformatterdded2d62025-04-28 00:27:21 +0000479
Sean McCullough8d93e362025-04-27 23:32:18 +0000480 // Render the mermaid diagram using promise
Autoformatterdded2d62025-04-28 00:27:21 +0000481 mermaid
482 .render(`${id}-svg`, code)
Sean McCullough8d93e362025-04-27 23:32:18 +0000483 .then(({ svg }) => {
484 container.innerHTML = svg;
485 })
Autoformatterdded2d62025-04-28 00:27:21 +0000486 .catch((err) => {
487 console.error("Error rendering mermaid diagram:", err);
Sean McCullough8d93e362025-04-27 23:32:18 +0000488 // Show the original code as fallback
489 container.innerHTML = `<pre>${code}</pre>`;
490 });
491 } catch (err) {
Autoformatterdded2d62025-04-28 00:27:21 +0000492 console.error("Error processing mermaid diagram:", err);
Sean McCullough8d93e362025-04-27 23:32:18 +0000493 // Show the original code as fallback
494 container.innerHTML = `<pre>${code}</pre>`;
495 }
496 });
497 }, 100); // Small delay to ensure DOM is ready
498 }
Sean McCullough86b56862025-04-18 13:04:03 -0700499
500 // See https://lit.dev/docs/components/lifecycle/
501 disconnectedCallback() {
502 super.disconnectedCallback();
503 }
504
505 renderMarkdown(markdownContent: string): string {
506 try {
Sean McCullough8d93e362025-04-27 23:32:18 +0000507 // Create a custom renderer
508 const renderer = new Renderer();
509 const originalCodeRenderer = renderer.code.bind(renderer);
Autoformatterdded2d62025-04-28 00:27:21 +0000510
Sean McCullough8d93e362025-04-27 23:32:18 +0000511 // Override the code renderer to handle mermaid diagrams
Autoformatterdded2d62025-04-28 00:27:21 +0000512 renderer.code = function ({ text, lang, escaped }: Tokens.Code): string {
513 if (lang === "mermaid") {
Sean McCullough8d93e362025-04-27 23:32:18 +0000514 // Generate a unique ID for this diagram
515 const id = `mermaid-diagram-${Math.random().toString(36).substring(2, 10)}`;
Autoformatterdded2d62025-04-28 00:27:21 +0000516
Sean McCullough8d93e362025-04-27 23:32:18 +0000517 // Just create the container and mermaid div - we'll render it in the updated() lifecycle method
518 return `<div class="mermaid-container">
519 <div class="mermaid" id="${id}">${text}</div>
520 </div>`;
521 }
522 // Default rendering for other code blocks
523 return originalCodeRenderer({ text, lang, escaped });
524 };
Autoformatterdded2d62025-04-28 00:27:21 +0000525
Sean McCullough86b56862025-04-18 13:04:03 -0700526 // Set markdown options for proper code block highlighting and safety
527 const markedOptions: MarkedOptions = {
528 gfm: true, // GitHub Flavored Markdown
529 breaks: true, // Convert newlines to <br>
530 async: false,
Autoformatterdded2d62025-04-28 00:27:21 +0000531 renderer: renderer,
Sean McCullough86b56862025-04-18 13:04:03 -0700532 // DOMPurify is recommended for production, but not included in this implementation
533 };
534 return marked.parse(markdownContent, markedOptions) as string;
535 } catch (error) {
536 console.error("Error rendering markdown:", error);
537 // Fallback to plain text if markdown parsing fails
538 return markdownContent;
539 }
540 }
541
542 /**
543 * Format timestamp for display
544 */
545 formatTimestamp(
546 timestamp: string | number | Date | null | undefined,
547 defaultValue: string = "",
548 ): string {
549 if (!timestamp) return defaultValue;
550 try {
551 const date = new Date(timestamp);
552 if (isNaN(date.getTime())) return defaultValue;
553
554 // Format: Mar 13, 2025 09:53:25 AM
555 return date.toLocaleString("en-US", {
556 month: "short",
557 day: "numeric",
558 year: "numeric",
559 hour: "numeric",
560 minute: "2-digit",
561 second: "2-digit",
562 hour12: true,
563 });
564 } catch (e) {
565 return defaultValue;
566 }
567 }
568
569 formatNumber(
570 num: number | null | undefined,
571 defaultValue: string = "0",
572 ): string {
573 if (num === undefined || num === null) return defaultValue;
574 try {
575 return num.toLocaleString();
576 } catch (e) {
577 return String(num);
578 }
579 }
580 formatCurrency(
581 num: number | string | null | undefined,
582 defaultValue: string = "$0.00",
583 isMessageLevel: boolean = false,
584 ): string {
585 if (num === undefined || num === null) return defaultValue;
586 try {
587 // Use 4 decimal places for message-level costs, 2 for totals
588 const decimalPlaces = isMessageLevel ? 4 : 2;
589 return `$${parseFloat(String(num)).toFixed(decimalPlaces)}`;
590 } catch (e) {
591 return defaultValue;
592 }
593 }
594
595 showCommit(commitHash: string) {
Sean McCullough71941bd2025-04-18 13:31:48 -0700596 this.dispatchEvent(
597 new CustomEvent("show-commit-diff", {
598 bubbles: true,
599 composed: true,
600 detail: { commitHash },
601 }),
602 );
Sean McCullough86b56862025-04-18 13:04:03 -0700603 }
604
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000605 copyToClipboard(text: string, event: Event) {
606 const element = event.currentTarget as HTMLElement;
607 const rect = element.getBoundingClientRect();
608
609 navigator.clipboard
610 .writeText(text)
611 .then(() => {
612 this.showFloatingMessage("Copied!", rect, "success");
613 })
614 .catch((err) => {
615 console.error("Failed to copy text: ", err);
616 this.showFloatingMessage("Failed to copy!", rect, "error");
617 });
618 }
619
620 showFloatingMessage(
621 message: string,
622 targetRect: DOMRect,
623 type: "success" | "error",
624 ) {
625 // Create floating message element
626 const floatingMsg = document.createElement("div");
627 floatingMsg.textContent = message;
628 floatingMsg.className = `floating-message ${type}`;
629
630 // Position it near the clicked element
631 // Position just above the element
632 const top = targetRect.top - 30;
633 const left = targetRect.left + targetRect.width / 2 - 40;
634
635 floatingMsg.style.position = "fixed";
636 floatingMsg.style.top = `${top}px`;
637 floatingMsg.style.left = `${left}px`;
638 floatingMsg.style.zIndex = "9999";
639
640 // Add to document body
641 document.body.appendChild(floatingMsg);
642
643 // Animate in
644 floatingMsg.style.opacity = "0";
645 floatingMsg.style.transform = "translateY(10px)";
646
647 setTimeout(() => {
648 floatingMsg.style.opacity = "1";
649 floatingMsg.style.transform = "translateY(0)";
650 }, 10);
651
652 // Remove after animation
653 setTimeout(() => {
654 floatingMsg.style.opacity = "0";
655 floatingMsg.style.transform = "translateY(-10px)";
656
657 setTimeout(() => {
658 document.body.removeChild(floatingMsg);
659 }, 300);
660 }, 1500);
661 }
662
Sean McCullough86b56862025-04-18 13:04:03 -0700663 render() {
664 return html`
665 <div
666 class="message ${this.message?.type} ${this.message?.end_of_turn
667 ? "end-of-turn"
668 : ""}"
669 >
670 ${this.previousMessage?.type != this.message?.type
671 ? html`<div class="message-icon">
672 ${this.message?.type.toUpperCase()[0]}
673 </div>`
674 : ""}
675 <div class="message-content">
676 <div class="message-header">
677 <span class="message-type">${this.message?.type}</span>
Sean McCullough71941bd2025-04-18 13:31:48 -0700678 <span class="message-timestamp"
679 >${this.formatTimestamp(this.message?.timestamp)}
680 ${this.message?.elapsed
681 ? html`(${(this.message?.elapsed / 1e9).toFixed(2)}s)`
682 : ""}</span
683 >
684 ${this.message?.usage
685 ? html` <span class="message-usage">
686 <span title="Input tokens"
Josh Bleecher Snyder35889972025-04-24 20:48:16 +0000687 >In:
Josh Bleecher Snydera0801ad2025-04-25 19:34:53 +0000688 ${this.formatNumber(
689 (this.message?.usage?.input_tokens || 0) +
Autoformatter5c70bfe2025-04-25 21:28:00 +0000690 (this.message?.usage?.cache_read_input_tokens || 0) +
691 (this.message?.usage?.cache_creation_input_tokens || 0),
Josh Bleecher Snydera0801ad2025-04-25 19:34:53 +0000692 )}</span
Sean McCullough71941bd2025-04-18 13:31:48 -0700693 >
Sean McCullough71941bd2025-04-18 13:31:48 -0700694 <span title="Output tokens"
Autoformatter5c70bfe2025-04-25 21:28:00 +0000695 >Out:
696 ${this.formatNumber(
697 this.message?.usage?.output_tokens,
698 )}</span
Sean McCullough71941bd2025-04-18 13:31:48 -0700699 >
700 <span title="Message cost"
701 >(${this.formatCurrency(
702 this.message?.usage?.cost_usd,
703 )})</span
704 >
705 </span>`
706 : ""}
Sean McCullough86b56862025-04-18 13:04:03 -0700707 </div>
708 <div class="message-text-container">
709 <div class="message-actions">
710 ${copyButton(this.message?.content)}
711 </div>
712 ${this.message?.content
713 ? html`
714 <div class="message-text markdown-content">
715 ${unsafeHTML(this.renderMarkdown(this.message?.content))}
716 </div>
717 `
718 : ""}
719 </div>
720 <sketch-tool-calls
721 .toolCalls=${this.message?.tool_calls}
Sean McCullough2deac842025-04-21 18:17:57 -0700722 .open=${this.open}
Sean McCullough86b56862025-04-18 13:04:03 -0700723 ></sketch-tool-calls>
724 ${this.message?.commits
725 ? html`
726 <div class="commits-container">
727 <div class="commits-header">
Sean McCullough71941bd2025-04-18 13:31:48 -0700728 ${this.message.commits.length} new
729 commit${this.message.commits.length > 1 ? "s" : ""} detected
Sean McCullough86b56862025-04-18 13:04:03 -0700730 </div>
731 ${this.message.commits.map((commit) => {
732 return html`
733 <div class="commit-boxes-row">
734 <div class="commit-box">
735 <div class="commit-preview">
Philip Zeyliger72682df2025-04-23 13:09:46 -0700736 <span
737 class="commit-hash"
738 title="Click to copy: ${commit.hash}"
739 @click=${(e) =>
740 this.copyToClipboard(
741 commit.hash.substring(0, 8),
742 e,
743 )}
744 >
Pokey Rule7be879f2025-04-23 15:30:15 +0100745 ${commit.hash.substring(0, 8)}
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000746 </span>
747 ${commit.pushed_branch
748 ? html`
Pokey Rule7be879f2025-04-23 15:30:15 +0100749 <span class="branch-wrapper">
750 (<span
751 class="commit-branch pushed-branch"
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000752 title="Click to copy: ${commit.pushed_branch}"
753 @click=${(e) =>
754 this.copyToClipboard(
755 commit.pushed_branch,
756 e,
757 )}
758 >${commit.pushed_branch}</span
Pokey Rule7be879f2025-04-23 15:30:15 +0100759 >)
760 </span>
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000761 `
762 : ``}
763 <span class="commit-subject"
764 >${commit.subject}</span
Sean McCullough71941bd2025-04-18 13:31:48 -0700765 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000766 <button
767 class="commit-diff-button"
768 @click=${() => this.showCommit(commit.hash)}
769 >
770 View Diff
771 </button>
Sean McCullough86b56862025-04-18 13:04:03 -0700772 </div>
773 <div class="commit-details is-hidden">
774 <pre>${commit.body}</pre>
775 </div>
Sean McCullough86b56862025-04-18 13:04:03 -0700776 </div>
777 </div>
778 `;
779 })}
780 </div>
781 `
782 : ""}
783 </div>
784 </div>
785 `;
786 }
787}
788
Sean McCullough71941bd2025-04-18 13:31:48 -0700789function copyButton(textToCopy: string) {
Sean McCullough86b56862025-04-18 13:04:03 -0700790 // Add click event listener to handle copying
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000791 const buttonClass = "copy-button";
792 const buttonContent = "Copy";
793 const successContent = "Copied!";
794 const failureContent = "Failed";
795
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700796 const ret = html`<button
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000797 class="${buttonClass}"
798 title="Copy to clipboard"
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700799 @click=${(e: Event) => {
800 e.stopPropagation();
801 const copyButton = e.currentTarget as HTMLButtonElement;
802 navigator.clipboard
803 .writeText(textToCopy)
804 .then(() => {
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000805 copyButton.textContent = successContent;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700806 setTimeout(() => {
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000807 copyButton.textContent = buttonContent;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700808 }, 2000);
809 })
810 .catch((err) => {
811 console.error("Failed to copy text: ", err);
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000812 copyButton.textContent = failureContent;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700813 setTimeout(() => {
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000814 copyButton.textContent = buttonContent;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700815 }, 2000);
816 });
817 }}
818 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000819 ${buttonContent}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -0700820 </button>`;
Sean McCullough86b56862025-04-18 13:04:03 -0700821
Sean McCullough71941bd2025-04-18 13:31:48 -0700822 return ret;
Sean McCullough86b56862025-04-18 13:04:03 -0700823}
824
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000825// Create global styles for floating messages
826const floatingMessageStyles = document.createElement("style");
827floatingMessageStyles.textContent = `
828 .floating-message {
829 background-color: rgba(0, 0, 0, 0.8);
830 color: white;
831 padding: 5px 10px;
832 border-radius: 4px;
833 font-size: 12px;
834 font-family: system-ui, sans-serif;
835 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
836 pointer-events: none;
837 transition: opacity 0.3s ease, transform 0.3s ease;
838 }
839
840 .floating-message.success {
841 background-color: rgba(40, 167, 69, 0.9);
842 }
843
844 .floating-message.error {
845 background-color: rgba(220, 53, 69, 0.9);
846 }
847`;
848document.head.appendChild(floatingMessageStyles);
849
Sean McCullough86b56862025-04-18 13:04:03 -0700850declare global {
851 interface HTMLElementTagNameMap {
852 "sketch-timeline-message": SketchTimelineMessage;
853 }
854}