blob: 0fa79fb6691325efec5a329cf74dd170182767e3 [file] [log] [blame]
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001import { css, html, LitElement, render } from "lit";
Sean McCullough86b56862025-04-18 13:04:03 -07002import { unsafeHTML } from "lit/directives/unsafe-html.js";
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00003import { customElement, property, state } 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";
Philip Zeyliger53ab2452025-06-04 17:49:33 +00007import DOMPurify from "dompurify";
Sean McCullough86b56862025-04-18 13:04:03 -07008import "./sketch-tool-calls";
9@customElement("sketch-timeline-message")
10export class SketchTimelineMessage extends LitElement {
11 @property()
Sean McCulloughd9f13372025-04-21 15:08:49 -070012 message: AgentMessage;
Sean McCullough86b56862025-04-18 13:04:03 -070013
14 @property()
Sean McCulloughd9f13372025-04-21 15:08:49 -070015 previousMessage: AgentMessage;
Sean McCullough86b56862025-04-18 13:04:03 -070016
Sean McCullough2deac842025-04-21 18:17:57 -070017 @property()
18 open: boolean = false;
19
Philip Zeyligerb8a8f352025-06-02 07:39:37 -070020 @property()
21 firstMessageIndex: number = 0;
22
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000023 @state()
24 showInfo: boolean = false;
25
Sean McCullough86b56862025-04-18 13:04:03 -070026 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
27 // Note that these styles only apply to the scope of this web component's
28 // shadow DOM node, so they won't leak out or collide with CSS declared in
29 // other components or the containing web page (...unless you want it to do that).
30 static styles = css`
31 .message {
32 position: relative;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000033 margin-bottom: 6px;
34 display: flex;
35 flex-direction: column;
36 width: 100%;
Sean McCullough86b56862025-04-18 13:04:03 -070037 }
38
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000039 .message-container {
40 display: flex;
41 position: relative;
42 width: 100%;
43 }
44
45 .message-metadata-left {
46 flex: 0 0 80px;
47 padding: 3px 5px;
48 text-align: right;
49 font-size: 11px;
50 color: #777;
51 align-self: flex-start;
52 }
53
54 .message-metadata-right {
55 flex: 0 0 80px;
56 padding: 3px 5px;
57 text-align: left;
58 font-size: 11px;
59 color: #777;
60 align-self: flex-start;
61 }
62
63 .message-bubble-container {
64 flex: 1;
65 display: flex;
66 max-width: calc(100% - 160px);
Philip Zeyligere31d2a92025-05-11 15:22:35 -070067 overflow: hidden;
68 text-overflow: ellipsis;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000069 }
70
71 .user .message-bubble-container {
72 justify-content: flex-end;
73 }
74
75 .agent .message-bubble-container,
76 .tool .message-bubble-container,
77 .error .message-bubble-container {
78 justify-content: flex-start;
Sean McCullough86b56862025-04-18 13:04:03 -070079 }
80
81 .message-content {
82 position: relative;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000083 padding: 6px 10px;
84 border-radius: 12px;
85 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
Philip Zeyligere31d2a92025-05-11 15:22:35 -070086 max-width: 100%;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000087 width: fit-content;
88 min-width: min-content;
Philip Zeyligere31d2a92025-05-11 15:22:35 -070089 overflow-wrap: break-word;
90 word-break: break-word;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000091 }
92
93 /* User message styling */
94 .user .message-content {
95 background-color: #2196f3;
96 color: white;
97 border-bottom-right-radius: 5px;
98 }
99
100 /* Agent message styling */
101 .agent .message-content,
102 .tool .message-content,
103 .error .message-content {
104 background-color: #f1f1f1;
105 color: black;
106 border-bottom-left-radius: 5px;
Sean McCullough86b56862025-04-18 13:04:03 -0700107 }
108
109 /* Copy button styles */
110 .message-text-container,
111 .tool-result-container {
112 position: relative;
113 }
114
115 .message-actions {
116 position: absolute;
117 top: 5px;
118 right: 5px;
119 z-index: 10;
120 opacity: 0;
121 transition: opacity 0.2s ease;
122 }
123
124 .message-text-container:hover .message-actions,
125 .tool-result-container:hover .message-actions {
126 opacity: 1;
127 }
128
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000129 .message-actions {
Sean McCullough86b56862025-04-18 13:04:03 -0700130 display: flex;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000131 gap: 6px;
132 }
133
134 .copy-icon,
135 .info-icon {
136 background-color: transparent;
137 border: none;
138 color: rgba(0, 0, 0, 0.6);
139 cursor: pointer;
140 padding: 3px;
141 border-radius: 50%;
142 display: flex;
143 align-items: center;
144 justify-content: center;
145 width: 24px;
146 height: 24px;
147 transition: all 0.15s ease;
148 }
149
150 .user .copy-icon,
151 .user .info-icon {
152 color: rgba(255, 255, 255, 0.8);
153 }
154
155 .copy-icon:hover,
156 .info-icon:hover {
157 background-color: rgba(0, 0, 0, 0.08);
158 }
159
160 .user .copy-icon:hover,
161 .user .info-icon:hover {
162 background-color: rgba(255, 255, 255, 0.15);
163 }
164
165 /* Message metadata styling */
166 .message-type {
167 font-weight: bold;
168 font-size: 11px;
Sean McCullough86b56862025-04-18 13:04:03 -0700169 }
170
171 .message-timestamp {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000172 display: block;
Sean McCullough86b56862025-04-18 13:04:03 -0700173 font-size: 10px;
174 color: #888;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000175 margin-top: 2px;
176 }
177
178 .message-duration {
179 display: block;
180 font-size: 10px;
181 color: #888;
182 margin-top: 2px;
Sean McCullough86b56862025-04-18 13:04:03 -0700183 }
184
185 .message-usage {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000186 display: block;
Sean McCullough86b56862025-04-18 13:04:03 -0700187 font-size: 10px;
188 color: #888;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000189 margin-top: 3px;
Sean McCullough86b56862025-04-18 13:04:03 -0700190 }
191
192 .conversation-id {
193 font-family: monospace;
194 font-size: 12px;
195 padding: 2px 4px;
Sean McCullough86b56862025-04-18 13:04:03 -0700196 margin-left: auto;
197 }
198
199 .parent-info {
200 font-size: 11px;
201 opacity: 0.8;
202 }
203
204 .subconversation {
205 border-left: 2px solid transparent;
206 padding-left: 5px;
207 margin-left: 20px;
208 transition: margin-left 0.3s ease;
209 }
210
211 .message-text {
212 overflow-x: auto;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000213 margin-bottom: 0;
214 font-family: sans-serif;
215 padding: 2px 0;
Sean McCullough86b56862025-04-18 13:04:03 -0700216 user-select: text;
217 cursor: text;
218 -webkit-user-select: text;
219 -moz-user-select: text;
220 -ms-user-select: text;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000221 font-size: 14px;
222 line-height: 1.35;
223 text-align: left;
224 }
225
226 /* Style for code blocks within messages */
227 .message-text pre,
228 .message-text code {
229 font-family: monospace;
230 background: rgba(0, 0, 0, 0.05);
231 border-radius: 4px;
232 padding: 2px 4px;
233 overflow-x: auto;
234 max-width: 100%;
235 white-space: pre-wrap; /* Allow wrapping for very long lines */
236 word-break: break-all; /* Break words at any character */
237 box-sizing: border-box; /* Include padding in width calculation */
238 }
239
Pokey Rulea10f1512025-05-15 13:53:26 +0000240 /* Code block container styles */
241 .code-block-container {
242 position: relative;
243 margin: 8px 0;
244 border-radius: 6px;
245 overflow: hidden;
246 background: rgba(0, 0, 0, 0.05);
247 }
248
249 .user .code-block-container {
250 background: rgba(255, 255, 255, 0.2);
251 }
252
253 .code-block-header {
254 display: flex;
255 justify-content: space-between;
256 align-items: center;
257 padding: 4px 8px;
258 background: rgba(0, 0, 0, 0.1);
259 font-size: 12px;
260 }
261
262 .user .code-block-header {
263 background: rgba(255, 255, 255, 0.2);
264 color: white;
265 }
266
267 .code-language {
268 font-family: monospace;
269 font-size: 11px;
270 font-weight: 500;
271 }
272
273 .code-copy-button {
274 background: transparent;
275 border: none;
276 color: inherit;
277 cursor: pointer;
278 padding: 2px;
279 border-radius: 3px;
280 display: flex;
281 align-items: center;
282 justify-content: center;
283 opacity: 0.7;
284 transition: all 0.15s ease;
285 }
286
287 .code-copy-button:hover {
288 opacity: 1;
289 background: rgba(0, 0, 0, 0.1);
290 }
291
292 .user .code-copy-button:hover {
293 background: rgba(255, 255, 255, 0.2);
294 }
295
296 .code-block-container pre {
297 margin: 0;
298 padding: 8px;
299 background: transparent;
300 }
301
302 .code-block-container code {
303 background: transparent;
304 padding: 0;
305 display: block;
306 width: 100%;
307 }
308
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000309 .user .message-text pre,
310 .user .message-text code {
311 background: rgba(255, 255, 255, 0.2);
312 color: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700313 }
314
315 .tool-details {
316 margin-top: 3px;
317 padding-top: 3px;
318 border-top: 1px dashed #e0e0e0;
319 font-size: 12px;
320 }
321
322 .tool-name {
323 font-size: 12px;
324 font-weight: bold;
325 margin-bottom: 2px;
326 background: #f0f0f0;
327 padding: 2px 4px;
328 border-radius: 2px;
329 display: flex;
330 align-items: center;
331 gap: 3px;
332 }
333
334 .tool-input,
335 .tool-result {
336 margin-top: 2px;
337 padding: 3px 5px;
338 background: #f7f7f7;
339 border-radius: 2px;
340 font-family: monospace;
341 font-size: 12px;
342 overflow-x: auto;
343 white-space: pre;
344 line-height: 1.3;
345 user-select: text;
346 cursor: text;
347 -webkit-user-select: text;
348 -moz-user-select: text;
349 -ms-user-select: text;
350 }
351
352 .tool-result {
353 max-height: 300px;
354 overflow-y: auto;
355 }
356
357 .usage-info {
358 margin-top: 10px;
359 padding-top: 10px;
360 border-top: 1px dashed #e0e0e0;
361 font-size: 12px;
362 color: #666;
363 }
364
365 /* Custom styles for IRC-like experience */
366 .user .message-content {
367 border-left-color: #2196f3;
368 }
369
370 .agent .message-content {
371 border-left-color: #4caf50;
372 }
373
374 .tool .message-content {
375 border-left-color: #ff9800;
376 }
377
378 .error .message-content {
379 border-left-color: #f44336;
380 }
381
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700382 /* Compact message styling - distinct visual separation */
383 .compact {
384 background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
385 border: 2px solid #fd7e14;
386 border-radius: 12px;
387 margin: 20px 0;
388 padding: 0;
389 }
390
391 .compact .message-content {
392 border-left: 4px solid #fd7e14;
393 background: rgba(253, 126, 20, 0.05);
394 font-weight: 500;
395 }
396
397 .compact .message-text {
398 color: #8b4513;
399 font-size: 13px;
400 line-height: 1.4;
401 }
402
403 .compact::before {
404 content: "📚 CONVERSATION EPOCH";
405 display: block;
406 text-align: center;
407 font-size: 11px;
408 font-weight: bold;
409 color: #8b4513;
410 background: #fd7e14;
411 color: white;
412 padding: 4px 8px;
413 margin: 0;
414 border-radius: 8px 8px 0 0;
415 letter-spacing: 1px;
416 }
417
418 /* Pre-compaction messages get a subtle diagonal stripe background */
419 .pre-compaction {
420 background: repeating-linear-gradient(
421 45deg,
422 #ffffff,
423 #ffffff 10px,
424 #f8f8f8 10px,
425 #f8f8f8 20px
426 );
427 opacity: 0.85;
428 border-left: 3px solid #ddd;
429 }
430
431 .pre-compaction .message-content {
432 background: rgba(255, 255, 255, 0.7);
433 backdrop-filter: blur(1px);
434 }
435
Sean McCullough86b56862025-04-18 13:04:03 -0700436 /* Make message type display bold but without the IRC-style markers */
437 .message-type {
438 font-weight: bold;
439 }
440
441 /* Commit message styling */
Sean McCullough86b56862025-04-18 13:04:03 -0700442 .commits-container {
443 margin-top: 10px;
Sean McCullough86b56862025-04-18 13:04:03 -0700444 }
445
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000446 .commit-notification {
447 background-color: #e8f5e9;
448 color: #2e7d32;
449 font-weight: 500;
450 font-size: 12px;
451 padding: 6px 10px;
452 border-radius: 10px;
453 margin-bottom: 8px;
454 text-align: center;
455 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
Sean McCullough86b56862025-04-18 13:04:03 -0700456 }
457
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000458 .commit-card {
459 background-color: #f5f5f5;
460 border-radius: 8px;
Sean McCullough86b56862025-04-18 13:04:03 -0700461 overflow: hidden;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000462 margin-bottom: 6px;
463 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
464 padding: 6px 8px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000465 display: flex;
466 align-items: center;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000467 gap: 8px;
Sean McCullough86b56862025-04-18 13:04:03 -0700468 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700469
Sean McCullough86b56862025-04-18 13:04:03 -0700470 .commit-hash {
471 color: #0366d6;
472 font-weight: bold;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000473 font-family: monospace;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000474 cursor: pointer;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000475 text-decoration: none;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000476 background-color: rgba(3, 102, 214, 0.08);
477 padding: 2px 5px;
478 border-radius: 4px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000479 }
480
481 .commit-hash:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000482 background-color: rgba(3, 102, 214, 0.15);
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000483 }
484
485 .commit-branch {
486 color: #28a745;
487 font-weight: 500;
488 cursor: pointer;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000489 font-family: monospace;
490 background-color: rgba(40, 167, 69, 0.08);
491 padding: 2px 5px;
492 border-radius: 4px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000493 }
494
495 .commit-branch:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000496 background-color: rgba(40, 167, 69, 0.15);
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000497 }
498
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000499 .commit-subject {
500 font-size: 13px;
501 color: #333;
502 flex-grow: 1;
503 overflow: hidden;
504 text-overflow: ellipsis;
505 white-space: nowrap;
Sean McCullough86b56862025-04-18 13:04:03 -0700506 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700507
Sean McCullough86b56862025-04-18 13:04:03 -0700508 .commit-diff-button {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000509 padding: 3px 8px;
510 border: none;
511 border-radius: 4px;
512 background-color: #0366d6;
513 color: white;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000514 font-size: 11px;
Sean McCullough86b56862025-04-18 13:04:03 -0700515 cursor: pointer;
516 transition: all 0.2s ease;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000517 display: block;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000518 margin-left: auto;
Sean McCullough86b56862025-04-18 13:04:03 -0700519 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700520
Sean McCullough86b56862025-04-18 13:04:03 -0700521 .commit-diff-button:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000522 background-color: #0256b4;
Sean McCullough86b56862025-04-18 13:04:03 -0700523 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700524
Sean McCullough86b56862025-04-18 13:04:03 -0700525 /* Tool call cards */
526 .tool-call-cards-container {
527 display: flex;
528 flex-direction: column;
529 gap: 8px;
530 margin-top: 8px;
531 }
532
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000533 /* Error message specific styling */
534 .error .message-content {
535 background-color: #ffebee;
536 border-left: 3px solid #f44336;
Sean McCullough86b56862025-04-18 13:04:03 -0700537 }
538
539 .end-of-turn {
540 margin-bottom: 15px;
541 }
542
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000543 .end-of-turn-indicator {
544 display: block;
545 font-size: 11px;
546 color: #777;
547 padding: 2px 0;
548 margin-top: 8px;
549 text-align: right;
550 font-style: italic;
551 }
552
553 .user .end-of-turn-indicator {
554 color: rgba(255, 255, 255, 0.7);
555 }
556
557 /* Message info panel styling */
558 .message-info-panel {
559 margin-top: 8px;
560 padding: 8px;
561 background-color: rgba(0, 0, 0, 0.03);
562 border-radius: 6px;
563 font-size: 12px;
564 transition: all 0.2s ease;
565 border-left: 2px solid rgba(0, 0, 0, 0.1);
566 }
567
568 .user .message-info-panel {
569 background-color: rgba(255, 255, 255, 0.15);
570 border-left: 2px solid rgba(255, 255, 255, 0.2);
571 }
572
573 .info-row {
574 margin-bottom: 3px;
575 display: flex;
576 }
577
578 .info-label {
579 font-weight: bold;
580 margin-right: 5px;
581 min-width: 60px;
582 }
583
584 .info-value {
585 flex: 1;
586 }
587
588 .conversation-id {
589 font-family: monospace;
Sean McCullough86b56862025-04-18 13:04:03 -0700590 font-size: 10px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000591 word-break: break-all;
Sean McCullough86b56862025-04-18 13:04:03 -0700592 }
593
594 .markdown-content {
595 box-sizing: border-box;
596 min-width: 200px;
597 margin: 0 auto;
598 }
599
600 .markdown-content p {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000601 margin-block-start: 0.3em;
602 margin-block-end: 0.3em;
603 }
604
605 .markdown-content p:first-child {
606 margin-block-start: 0;
607 }
608
609 .markdown-content p:last-child {
610 margin-block-end: 0;
611 }
612
613 /* Styling for markdown elements */
614 .markdown-content a {
615 color: inherit;
616 text-decoration: underline;
617 }
618
619 .user .markdown-content a {
620 color: #fff;
621 text-decoration: underline;
622 }
623
624 .markdown-content ul,
625 .markdown-content ol {
626 padding-left: 1.5em;
627 margin: 0.5em 0;
628 }
629
630 .markdown-content blockquote {
631 border-left: 3px solid rgba(0, 0, 0, 0.2);
632 padding-left: 1em;
633 margin-left: 0.5em;
634 font-style: italic;
635 }
636
637 .user .markdown-content blockquote {
638 border-left: 3px solid rgba(255, 255, 255, 0.4);
Sean McCullough86b56862025-04-18 13:04:03 -0700639 }
Autoformatterdded2d62025-04-28 00:27:21 +0000640
Sean McCullough8d93e362025-04-27 23:32:18 +0000641 /* Mermaid diagram styling */
642 .mermaid-container {
643 margin: 1em 0;
644 padding: 0.5em;
645 background-color: #f8f8f8;
646 border-radius: 4px;
647 overflow-x: auto;
648 }
Autoformatterdded2d62025-04-28 00:27:21 +0000649
Sean McCullough8d93e362025-04-27 23:32:18 +0000650 .mermaid {
651 text-align: center;
652 }
Sean McCullough86b56862025-04-18 13:04:03 -0700653 `;
654
Sean McCullough8d93e362025-04-27 23:32:18 +0000655 // Track mermaid diagrams that need rendering
656 private mermaidDiagrams = new Map();
657
Sean McCullough86b56862025-04-18 13:04:03 -0700658 constructor() {
659 super();
Sean McCullough8d93e362025-04-27 23:32:18 +0000660 // Initialize mermaid with specific config
661 mermaid.initialize({
662 startOnLoad: false,
Sean McCulloughf98d7302025-04-27 17:44:06 -0700663 suppressErrorRendering: true,
Autoformatterdded2d62025-04-28 00:27:21 +0000664 theme: "default",
665 securityLevel: "loose", // Allows more flexibility but be careful with user-generated content
666 fontFamily: "monospace",
Sean McCullough8d93e362025-04-27 23:32:18 +0000667 });
Sean McCullough86b56862025-04-18 13:04:03 -0700668 }
669
670 // See https://lit.dev/docs/components/lifecycle/
671 connectedCallback() {
672 super.connectedCallback();
673 }
Autoformatterdded2d62025-04-28 00:27:21 +0000674
Sean McCullough8d93e362025-04-27 23:32:18 +0000675 // After the component is updated and rendered, render any mermaid diagrams
676 updated(changedProperties: Map<string, unknown>) {
677 super.updated(changedProperties);
678 this.renderMermaidDiagrams();
Pokey Rulea10f1512025-05-15 13:53:26 +0000679 this.setupCodeBlockCopyButtons();
Sean McCullough8d93e362025-04-27 23:32:18 +0000680 }
Autoformatterdded2d62025-04-28 00:27:21 +0000681
Sean McCullough8d93e362025-04-27 23:32:18 +0000682 // Render mermaid diagrams after the component is updated
683 renderMermaidDiagrams() {
684 // Add a small delay to ensure the DOM is fully rendered
685 setTimeout(() => {
686 // Find all mermaid containers in our shadow root
Autoformatterdded2d62025-04-28 00:27:21 +0000687 const containers = this.shadowRoot?.querySelectorAll(".mermaid");
Sean McCullough8d93e362025-04-27 23:32:18 +0000688 if (!containers || containers.length === 0) return;
Autoformatterdded2d62025-04-28 00:27:21 +0000689
Sean McCullough8d93e362025-04-27 23:32:18 +0000690 // Process each mermaid diagram
Autoformatterdded2d62025-04-28 00:27:21 +0000691 containers.forEach((container) => {
Sean McCullough8d93e362025-04-27 23:32:18 +0000692 const id = container.id;
Autoformatterdded2d62025-04-28 00:27:21 +0000693 const code = container.textContent || "";
Sean McCullough8d93e362025-04-27 23:32:18 +0000694 if (!code || !id) return; // Use return for forEach instead of continue
Autoformatterdded2d62025-04-28 00:27:21 +0000695
Sean McCullough8d93e362025-04-27 23:32:18 +0000696 try {
697 // Clear any previous content
698 container.innerHTML = code;
Autoformatterdded2d62025-04-28 00:27:21 +0000699
Sean McCullough8d93e362025-04-27 23:32:18 +0000700 // Render the mermaid diagram using promise
Autoformatterdded2d62025-04-28 00:27:21 +0000701 mermaid
702 .render(`${id}-svg`, code)
Sean McCullough8d93e362025-04-27 23:32:18 +0000703 .then(({ svg }) => {
704 container.innerHTML = svg;
705 })
Autoformatterdded2d62025-04-28 00:27:21 +0000706 .catch((err) => {
707 console.error("Error rendering mermaid diagram:", err);
Sean McCullough8d93e362025-04-27 23:32:18 +0000708 // Show the original code as fallback
709 container.innerHTML = `<pre>${code}</pre>`;
710 });
711 } catch (err) {
Autoformatterdded2d62025-04-28 00:27:21 +0000712 console.error("Error processing mermaid diagram:", err);
Sean McCullough8d93e362025-04-27 23:32:18 +0000713 // Show the original code as fallback
714 container.innerHTML = `<pre>${code}</pre>`;
715 }
716 });
717 }, 100); // Small delay to ensure DOM is ready
718 }
Sean McCullough86b56862025-04-18 13:04:03 -0700719
Pokey Rulea10f1512025-05-15 13:53:26 +0000720 // Setup code block copy buttons after component is updated
721 setupCodeBlockCopyButtons() {
722 setTimeout(() => {
723 // Find all copy buttons in code blocks
724 const copyButtons =
725 this.shadowRoot?.querySelectorAll(".code-copy-button");
726 if (!copyButtons || copyButtons.length === 0) return;
727
728 // Add click event listener to each button
729 copyButtons.forEach((button) => {
730 button.addEventListener("click", (e) => {
731 e.stopPropagation();
732 const codeId = (button as HTMLElement).dataset.codeId;
733 if (!codeId) return;
734
735 const codeElement = this.shadowRoot?.querySelector(`#${codeId}`);
736 if (!codeElement) return;
737
738 const codeText = codeElement.textContent || "";
739 const buttonRect = button.getBoundingClientRect();
740
741 // Copy code to clipboard
742 navigator.clipboard
743 .writeText(codeText)
744 .then(() => {
745 // Show success indicator
746 const originalHTML = button.innerHTML;
747 button.innerHTML = `
748 <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">
749 <path d="M20 6L9 17l-5-5"></path>
750 </svg>
751 `;
752
753 // Display floating message
754 this.showFloatingMessage("Copied!", buttonRect, "success");
755
756 // Reset button after delay
757 setTimeout(() => {
758 button.innerHTML = originalHTML;
759 }, 2000);
760 })
761 .catch((err) => {
762 console.error("Failed to copy code:", err);
763 this.showFloatingMessage("Failed to copy!", buttonRect, "error");
764 });
765 });
766 });
767 }, 100); // Small delay to ensure DOM is ready
768 }
769
Sean McCullough86b56862025-04-18 13:04:03 -0700770 // See https://lit.dev/docs/components/lifecycle/
771 disconnectedCallback() {
772 super.disconnectedCallback();
773 }
774
775 renderMarkdown(markdownContent: string): string {
776 try {
Sean McCullough8d93e362025-04-27 23:32:18 +0000777 // Create a custom renderer
778 const renderer = new Renderer();
779 const originalCodeRenderer = renderer.code.bind(renderer);
Autoformatterdded2d62025-04-28 00:27:21 +0000780
Pokey Rulea10f1512025-05-15 13:53:26 +0000781 // Override the code renderer to handle mermaid diagrams and add copy buttons
Autoformatterdded2d62025-04-28 00:27:21 +0000782 renderer.code = function ({ text, lang, escaped }: Tokens.Code): string {
783 if (lang === "mermaid") {
Sean McCullough8d93e362025-04-27 23:32:18 +0000784 // Generate a unique ID for this diagram
785 const id = `mermaid-diagram-${Math.random().toString(36).substring(2, 10)}`;
Autoformatterdded2d62025-04-28 00:27:21 +0000786
Sean McCullough8d93e362025-04-27 23:32:18 +0000787 // Just create the container and mermaid div - we'll render it in the updated() lifecycle method
788 return `<div class="mermaid-container">
789 <div class="mermaid" id="${id}">${text}</div>
790 </div>`;
791 }
Pokey Rulea10f1512025-05-15 13:53:26 +0000792
Philip Zeyliger0d092842025-06-09 18:57:12 -0700793 // For regular code blocks, call the original renderer to get properly escaped HTML
794 const originalCodeHtml = originalCodeRenderer({ text, lang, escaped });
795
796 // Extract the code content from the original HTML to add our custom wrapper
797 // The original renderer returns: <pre><code class="language-x">escapedText</code></pre>
798 const codeMatch = originalCodeHtml.match(
799 /<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/,
800 );
801 if (!codeMatch) {
802 // Fallback to original if we can't parse it
803 return originalCodeHtml;
804 }
805
806 const escapedText = codeMatch[1];
Pokey Rulea10f1512025-05-15 13:53:26 +0000807 const id = `code-block-${Math.random().toString(36).substring(2, 10)}`;
808 const langClass = lang ? ` class="language-${lang}"` : "";
809
810 return `<div class="code-block-container">
811 <div class="code-block-header">
812 ${lang ? `<span class="code-language">${lang}</span>` : ""}
813 <button class="code-copy-button" title="Copy code" data-code-id="${id}">
814 <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">
815 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
816 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
817 </svg>
818 </button>
819 </div>
Philip Zeyliger0d092842025-06-09 18:57:12 -0700820 <pre><code id="${id}"${langClass}>${escapedText}</code></pre>
Pokey Rulea10f1512025-05-15 13:53:26 +0000821 </div>`;
Sean McCullough8d93e362025-04-27 23:32:18 +0000822 };
Autoformatterdded2d62025-04-28 00:27:21 +0000823
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000824 // Set markdown options for proper code block highlighting
Sean McCullough86b56862025-04-18 13:04:03 -0700825 const markedOptions: MarkedOptions = {
826 gfm: true, // GitHub Flavored Markdown
827 breaks: true, // Convert newlines to <br>
828 async: false,
Autoformatterdded2d62025-04-28 00:27:21 +0000829 renderer: renderer,
Sean McCullough86b56862025-04-18 13:04:03 -0700830 };
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000831
832 // Parse markdown and sanitize the output HTML with DOMPurify
833 const htmlOutput = marked.parse(markdownContent, markedOptions) as string;
834 return DOMPurify.sanitize(htmlOutput, {
835 // Allow common HTML elements that are safe
836 ALLOWED_TAGS: [
837 "p",
838 "br",
839 "strong",
840 "em",
841 "b",
842 "i",
843 "u",
844 "s",
845 "code",
846 "pre",
847 "h1",
848 "h2",
849 "h3",
850 "h4",
851 "h5",
852 "h6",
853 "ul",
854 "ol",
855 "li",
856 "blockquote",
857 "a",
858 "div",
859 "span", // For mermaid diagrams and code blocks
860 "svg",
861 "g",
862 "path",
863 "rect",
864 "circle",
865 "text",
866 "line",
867 "polygon", // For mermaid SVG
868 "button", // For code copy buttons
869 ],
870 ALLOWED_ATTR: [
871 "href",
872 "title",
873 "target",
874 "rel", // For links
875 "class",
876 "id", // For styling and functionality
877 "data-*", // For code copy buttons
878 // SVG attributes for mermaid diagrams
879 "viewBox",
880 "width",
881 "height",
882 "xmlns",
883 "fill",
884 "stroke",
885 "stroke-width",
886 "d",
887 "x",
888 "y",
889 "x1",
890 "y1",
891 "x2",
892 "y2",
893 "cx",
894 "cy",
895 "r",
896 "rx",
897 "ry",
898 "points",
899 "transform",
900 "text-anchor",
901 "font-size",
902 "font-family",
903 ],
904 // Allow data attributes for functionality
905 ALLOW_DATA_ATTR: true,
906 // Keep whitespace for code formatting
907 KEEP_CONTENT: true,
908 });
Sean McCullough86b56862025-04-18 13:04:03 -0700909 } catch (error) {
910 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000911 // Fallback to sanitized plain text if markdown parsing fails
912 return DOMPurify.sanitize(markdownContent);
Sean McCullough86b56862025-04-18 13:04:03 -0700913 }
914 }
915
916 /**
917 * Format timestamp for display
918 */
919 formatTimestamp(
920 timestamp: string | number | Date | null | undefined,
921 defaultValue: string = "",
922 ): string {
923 if (!timestamp) return defaultValue;
924 try {
925 const date = new Date(timestamp);
926 if (isNaN(date.getTime())) return defaultValue;
927
928 // Format: Mar 13, 2025 09:53:25 AM
929 return date.toLocaleString("en-US", {
930 month: "short",
931 day: "numeric",
932 year: "numeric",
933 hour: "numeric",
934 minute: "2-digit",
935 second: "2-digit",
936 hour12: true,
937 });
938 } catch (e) {
939 return defaultValue;
940 }
941 }
942
943 formatNumber(
944 num: number | null | undefined,
945 defaultValue: string = "0",
946 ): string {
947 if (num === undefined || num === null) return defaultValue;
948 try {
949 return num.toLocaleString();
950 } catch (e) {
951 return String(num);
952 }
953 }
954 formatCurrency(
955 num: number | string | null | undefined,
956 defaultValue: string = "$0.00",
957 isMessageLevel: boolean = false,
958 ): string {
959 if (num === undefined || num === null) return defaultValue;
960 try {
961 // Use 4 decimal places for message-level costs, 2 for totals
962 const decimalPlaces = isMessageLevel ? 4 : 2;
963 return `$${parseFloat(String(num)).toFixed(decimalPlaces)}`;
964 } catch (e) {
965 return defaultValue;
966 }
967 }
968
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000969 // Format duration from nanoseconds to a human-readable string
970 _formatDuration(nanoseconds: number | null | undefined): string {
971 if (!nanoseconds) return "0s";
972
973 const seconds = nanoseconds / 1e9;
974
975 if (seconds < 60) {
976 return `${seconds.toFixed(1)}s`;
977 } else if (seconds < 3600) {
978 const minutes = Math.floor(seconds / 60);
979 const remainingSeconds = seconds % 60;
980 return `${minutes}min ${remainingSeconds.toFixed(0)}s`;
981 } else {
982 const hours = Math.floor(seconds / 3600);
983 const remainingSeconds = seconds % 3600;
984 const minutes = Math.floor(remainingSeconds / 60);
985 return `${hours}h ${minutes}min`;
986 }
987 }
988
Sean McCullough86b56862025-04-18 13:04:03 -0700989 showCommit(commitHash: string) {
Sean McCullough71941bd2025-04-18 13:31:48 -0700990 this.dispatchEvent(
991 new CustomEvent("show-commit-diff", {
992 bubbles: true,
993 composed: true,
994 detail: { commitHash },
995 }),
996 );
Sean McCullough86b56862025-04-18 13:04:03 -0700997 }
998
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000999 _toggleInfo(e: Event) {
1000 e.stopPropagation();
1001 this.showInfo = !this.showInfo;
1002 }
1003
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001004 copyToClipboard(text: string, event: Event) {
1005 const element = event.currentTarget as HTMLElement;
1006 const rect = element.getBoundingClientRect();
1007
1008 navigator.clipboard
1009 .writeText(text)
1010 .then(() => {
1011 this.showFloatingMessage("Copied!", rect, "success");
1012 })
1013 .catch((err) => {
1014 console.error("Failed to copy text: ", err);
1015 this.showFloatingMessage("Failed to copy!", rect, "error");
1016 });
1017 }
1018
1019 showFloatingMessage(
1020 message: string,
1021 targetRect: DOMRect,
1022 type: "success" | "error",
1023 ) {
1024 // Create floating message element
1025 const floatingMsg = document.createElement("div");
1026 floatingMsg.textContent = message;
1027 floatingMsg.className = `floating-message ${type}`;
1028
1029 // Position it near the clicked element
1030 // Position just above the element
1031 const top = targetRect.top - 30;
1032 const left = targetRect.left + targetRect.width / 2 - 40;
1033
1034 floatingMsg.style.position = "fixed";
1035 floatingMsg.style.top = `${top}px`;
1036 floatingMsg.style.left = `${left}px`;
1037 floatingMsg.style.zIndex = "9999";
1038
1039 // Add to document body
1040 document.body.appendChild(floatingMsg);
1041
1042 // Animate in
1043 floatingMsg.style.opacity = "0";
1044 floatingMsg.style.transform = "translateY(10px)";
1045
1046 setTimeout(() => {
1047 floatingMsg.style.opacity = "1";
1048 floatingMsg.style.transform = "translateY(0)";
1049 }, 10);
1050
1051 // Remove after animation
1052 setTimeout(() => {
1053 floatingMsg.style.opacity = "0";
1054 floatingMsg.style.transform = "translateY(-10px)";
1055
1056 setTimeout(() => {
1057 document.body.removeChild(floatingMsg);
1058 }, 300);
1059 }, 1500);
1060 }
1061
Sean McCullough86b56862025-04-18 13:04:03 -07001062 render() {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001063 // Calculate if this is an end of turn message with no parent conversation ID
1064 const isEndOfTurn =
1065 this.message?.end_of_turn && !this.message?.parent_conversation_id;
1066
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001067 const isPreCompaction =
1068 this.message?.idx !== undefined &&
1069 this.message.idx < this.firstMessageIndex;
1070
Sean McCullough86b56862025-04-18 13:04:03 -07001071 return html`
1072 <div
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001073 class="message ${this.message?.type} ${isEndOfTurn
Sean McCullough86b56862025-04-18 13:04:03 -07001074 ? "end-of-turn"
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001075 : ""} ${isPreCompaction ? "pre-compaction" : ""}"
Sean McCullough86b56862025-04-18 13:04:03 -07001076 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001077 <div class="message-container">
1078 <!-- Left area (empty for simplicity) -->
1079 <div class="message-metadata-left"></div>
1080
1081 <!-- Message bubble -->
1082 <div class="message-bubble-container">
1083 <div class="message-content">
1084 <div class="message-text-container">
1085 <div class="message-actions">
1086 ${copyButton(this.message?.content)}
1087 <button
1088 class="info-icon"
1089 title="Show message details"
1090 @click=${this._toggleInfo}
Sean McCullough71941bd2025-04-18 13:31:48 -07001091 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001092 <svg
1093 xmlns="http://www.w3.org/2000/svg"
1094 width="16"
1095 height="16"
1096 viewBox="0 0 24 24"
1097 fill="none"
1098 stroke="currentColor"
1099 stroke-width="2"
1100 stroke-linecap="round"
1101 stroke-linejoin="round"
1102 >
1103 <circle cx="12" cy="12" r="10"></circle>
1104 <line x1="12" y1="16" x2="12" y2="12"></line>
1105 <line x1="12" y1="8" x2="12.01" y2="8"></line>
1106 </svg>
1107 </button>
1108 </div>
1109 ${this.message?.content
1110 ? html`
1111 <div class="message-text markdown-content">
1112 ${unsafeHTML(
1113 this.renderMarkdown(this.message?.content),
1114 )}
1115 </div>
1116 `
1117 : ""}
1118
1119 <!-- End of turn indicator inside the bubble -->
1120 ${isEndOfTurn && this.message?.elapsed
1121 ? html`
1122 <div class="end-of-turn-indicator">
1123 end of turn
1124 (${this._formatDuration(this.message?.elapsed)})
1125 </div>
1126 `
1127 : ""}
1128
1129 <!-- Info panel that can be toggled -->
1130 ${this.showInfo
1131 ? html`
1132 <div class="message-info-panel">
1133 <div class="info-row">
1134 <span class="info-label">Type:</span>
1135 <span class="info-value">${this.message?.type}</span>
1136 </div>
1137 <div class="info-row">
1138 <span class="info-label">Time:</span>
1139 <span class="info-value"
1140 >${this.formatTimestamp(
1141 this.message?.timestamp,
1142 "",
1143 )}</span
1144 >
1145 </div>
1146 ${this.message?.elapsed
1147 ? html`
1148 <div class="info-row">
1149 <span class="info-label">Duration:</span>
1150 <span class="info-value"
1151 >${this._formatDuration(
1152 this.message?.elapsed,
1153 )}</span
1154 >
1155 </div>
1156 `
1157 : ""}
1158 ${this.message?.usage
1159 ? html`
1160 <div class="info-row">
1161 <span class="info-label">Tokens:</span>
1162 <span class="info-value">
1163 ${this.message?.usage
1164 ? html`
1165 <div>
1166 Input:
1167 ${this.formatNumber(
1168 this.message?.usage?.input_tokens ||
1169 0,
1170 )}
1171 </div>
1172 ${this.message?.usage
1173 ?.cache_creation_input_tokens
1174 ? html`
1175 <div>
1176 Cache creation:
1177 ${this.formatNumber(
1178 this.message?.usage
1179 ?.cache_creation_input_tokens,
1180 )}
1181 </div>
1182 `
1183 : ""}
1184 ${this.message?.usage
1185 ?.cache_read_input_tokens
1186 ? html`
1187 <div>
1188 Cache read:
1189 ${this.formatNumber(
1190 this.message?.usage
1191 ?.cache_read_input_tokens,
1192 )}
1193 </div>
1194 `
1195 : ""}
1196 <div>
1197 Output:
1198 ${this.formatNumber(
1199 this.message?.usage?.output_tokens,
1200 )}
1201 </div>
1202 <div>
1203 Cost:
1204 ${this.formatCurrency(
1205 this.message?.usage?.cost_usd,
1206 )}
1207 </div>
1208 `
1209 : "N/A"}
1210 </span>
1211 </div>
1212 `
1213 : ""}
1214 ${this.message?.conversation_id
1215 ? html`
1216 <div class="info-row">
1217 <span class="info-label">Conversation ID:</span>
1218 <span class="info-value conversation-id"
1219 >${this.message?.conversation_id}</span
1220 >
1221 </div>
1222 `
1223 : ""}
1224 </div>
1225 `
1226 : ""}
1227 </div>
1228
1229 <!-- Tool calls - only shown for agent messages -->
1230 ${this.message?.type === "agent"
1231 ? html`
1232 <sketch-tool-calls
1233 .toolCalls=${this.message?.tool_calls}
1234 .open=${this.open}
1235 ></sketch-tool-calls>
1236 `
1237 : ""}
1238
1239 <!-- Commits section (redesigned as bubbles) -->
1240 ${this.message?.commits
1241 ? html`
1242 <div class="commits-container">
1243 <div class="commit-notification">
1244 ${this.message.commits.length} new
1245 commit${this.message.commits.length > 1 ? "s" : ""}
1246 detected
1247 </div>
1248 ${this.message.commits.map((commit) => {
1249 return html`
1250 <div class="commit-card">
Philip Zeyliger72682df2025-04-23 13:09:46 -07001251 <span
1252 class="commit-hash"
1253 title="Click to copy: ${commit.hash}"
1254 @click=${(e) =>
1255 this.copyToClipboard(
1256 commit.hash.substring(0, 8),
1257 e,
1258 )}
1259 >
Pokey Rule7be879f2025-04-23 15:30:15 +01001260 ${commit.hash.substring(0, 8)}
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001261 </span>
1262 ${commit.pushed_branch
1263 ? html`
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001264 <span
1265 class="commit-branch pushed-branch"
1266 title="Click to copy: ${commit.pushed_branch}"
1267 @click=${(e) =>
1268 this.copyToClipboard(
1269 commit.pushed_branch,
1270 e,
1271 )}
1272 >${commit.pushed_branch}</span
1273 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001274 `
1275 : ``}
1276 <span class="commit-subject"
1277 >${commit.subject}</span
Sean McCullough71941bd2025-04-18 13:31:48 -07001278 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001279 <button
1280 class="commit-diff-button"
1281 @click=${() => this.showCommit(commit.hash)}
1282 >
1283 View Diff
1284 </button>
Sean McCullough86b56862025-04-18 13:04:03 -07001285 </div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001286 `;
1287 })}
1288 </div>
1289 `
1290 : ""}
1291 </div>
1292 </div>
1293
1294 <!-- Right side (empty for consistency) -->
1295 <div class="message-metadata-right"></div>
Sean McCullough86b56862025-04-18 13:04:03 -07001296 </div>
1297 </div>
1298 `;
1299 }
1300}
1301
Sean McCullough71941bd2025-04-18 13:31:48 -07001302function copyButton(textToCopy: string) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001303 // Use an icon of overlapping rectangles for copy
1304 const buttonClass = "copy-icon";
1305
1306 // SVG for copy icon (two overlapping rectangles)
1307 const copyIcon = html`<svg
1308 xmlns="http://www.w3.org/2000/svg"
1309 width="16"
1310 height="16"
1311 viewBox="0 0 24 24"
1312 fill="none"
1313 stroke="currentColor"
1314 stroke-width="2"
1315 stroke-linecap="round"
1316 stroke-linejoin="round"
1317 >
1318 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1319 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1320 </svg>`;
1321
1322 // SVG for success check mark
1323 const successIcon = html`<svg
1324 xmlns="http://www.w3.org/2000/svg"
1325 width="16"
1326 height="16"
1327 viewBox="0 0 24 24"
1328 fill="none"
1329 stroke="currentColor"
1330 stroke-width="2"
1331 stroke-linecap="round"
1332 stroke-linejoin="round"
1333 >
1334 <path d="M20 6L9 17l-5-5"></path>
1335 </svg>`;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001336
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001337 const ret = html`<button
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001338 class="${buttonClass}"
1339 title="Copy to clipboard"
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001340 @click=${(e: Event) => {
1341 e.stopPropagation();
1342 const copyButton = e.currentTarget as HTMLButtonElement;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001343 const originalInnerHTML = copyButton.innerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001344 navigator.clipboard
1345 .writeText(textToCopy)
1346 .then(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001347 copyButton.innerHTML = "";
1348 const successElement = document.createElement("div");
1349 copyButton.appendChild(successElement);
1350 render(successIcon, successElement);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001351 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001352 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001353 }, 2000);
1354 })
1355 .catch((err) => {
1356 console.error("Failed to copy text: ", err);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001357 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001358 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001359 }, 2000);
1360 });
1361 }}
1362 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001363 ${copyIcon}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001364 </button>`;
Sean McCullough86b56862025-04-18 13:04:03 -07001365
Sean McCullough71941bd2025-04-18 13:31:48 -07001366 return ret;
Sean McCullough86b56862025-04-18 13:04:03 -07001367}
1368
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001369// Create global styles for floating messages
1370const floatingMessageStyles = document.createElement("style");
1371floatingMessageStyles.textContent = `
1372 .floating-message {
1373 background-color: rgba(0, 0, 0, 0.8);
1374 color: white;
1375 padding: 5px 10px;
1376 border-radius: 4px;
1377 font-size: 12px;
1378 font-family: system-ui, sans-serif;
1379 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
1380 pointer-events: none;
1381 transition: opacity 0.3s ease, transform 0.3s ease;
1382 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001383
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001384 .floating-message.success {
1385 background-color: rgba(40, 167, 69, 0.9);
1386 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001387
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001388 .floating-message.error {
1389 background-color: rgba(220, 53, 69, 0.9);
1390 }
Philip Zeyligere31d2a92025-05-11 15:22:35 -07001391
1392 /* Style for code, pre elements, and tool components to ensure proper wrapping/truncation */
1393 pre, code, sketch-tool-calls, sketch-tool-card, sketch-tool-card-bash {
1394 white-space: nowrap;
1395 overflow: hidden;
1396 text-overflow: ellipsis;
1397 max-width: 100%;
1398 }
1399
1400 /* Special rule for the message content container */
1401 .message-content {
1402 max-width: 100% !important;
1403 overflow: hidden !important;
1404 }
1405
1406 /* Ensure tool call containers don't overflow */
1407 ::slotted(sketch-tool-calls) {
1408 max-width: 100%;
1409 width: 100%;
1410 overflow-wrap: break-word;
1411 word-break: break-word;
1412 }
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001413`;
1414document.head.appendChild(floatingMessageStyles);
1415
Sean McCullough86b56862025-04-18 13:04:03 -07001416declare global {
1417 interface HTMLElementTagNameMap {
1418 "sketch-timeline-message": SketchTimelineMessage;
1419 }
1420}