blob: 3fe519f50f51f1f9cea25fe41c1c5db60f98dc0e [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);
Philip Zeyliger57d28bc2025-06-06 20:28:34 +0000434 color: #333; /* Ensure dark text for readability */
435 }
436
437 .pre-compaction .message-text {
438 color: #333; /* Ensure dark text in message content */
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700439 }
440
Sean McCullough86b56862025-04-18 13:04:03 -0700441 /* Make message type display bold but without the IRC-style markers */
442 .message-type {
443 font-weight: bold;
444 }
445
446 /* Commit message styling */
Sean McCullough86b56862025-04-18 13:04:03 -0700447 .commits-container {
448 margin-top: 10px;
Sean McCullough86b56862025-04-18 13:04:03 -0700449 }
450
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000451 .commit-notification {
452 background-color: #e8f5e9;
453 color: #2e7d32;
454 font-weight: 500;
455 font-size: 12px;
456 padding: 6px 10px;
457 border-radius: 10px;
458 margin-bottom: 8px;
459 text-align: center;
460 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
Sean McCullough86b56862025-04-18 13:04:03 -0700461 }
462
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000463 .commit-card {
464 background-color: #f5f5f5;
465 border-radius: 8px;
Sean McCullough86b56862025-04-18 13:04:03 -0700466 overflow: hidden;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000467 margin-bottom: 6px;
468 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
469 padding: 6px 8px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000470 display: flex;
471 align-items: center;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000472 gap: 8px;
Sean McCullough86b56862025-04-18 13:04:03 -0700473 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700474
Sean McCullough86b56862025-04-18 13:04:03 -0700475 .commit-hash {
476 color: #0366d6;
477 font-weight: bold;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000478 font-family: monospace;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000479 cursor: pointer;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000480 text-decoration: none;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000481 background-color: rgba(3, 102, 214, 0.08);
482 padding: 2px 5px;
483 border-radius: 4px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000484 }
485
486 .commit-hash:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000487 background-color: rgba(3, 102, 214, 0.15);
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000488 }
489
490 .commit-branch {
491 color: #28a745;
492 font-weight: 500;
493 cursor: pointer;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000494 font-family: monospace;
495 background-color: rgba(40, 167, 69, 0.08);
496 padding: 2px 5px;
497 border-radius: 4px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000498 }
499
500 .commit-branch:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000501 background-color: rgba(40, 167, 69, 0.15);
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000502 }
503
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000504 .commit-subject {
505 font-size: 13px;
506 color: #333;
507 flex-grow: 1;
508 overflow: hidden;
509 text-overflow: ellipsis;
510 white-space: nowrap;
Sean McCullough86b56862025-04-18 13:04:03 -0700511 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700512
Sean McCullough86b56862025-04-18 13:04:03 -0700513 .commit-diff-button {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000514 padding: 3px 8px;
515 border: none;
516 border-radius: 4px;
517 background-color: #0366d6;
518 color: white;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000519 font-size: 11px;
Sean McCullough86b56862025-04-18 13:04:03 -0700520 cursor: pointer;
521 transition: all 0.2s ease;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000522 display: block;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000523 margin-left: auto;
Sean McCullough86b56862025-04-18 13:04:03 -0700524 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700525
Sean McCullough86b56862025-04-18 13:04:03 -0700526 .commit-diff-button:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000527 background-color: #0256b4;
Sean McCullough86b56862025-04-18 13:04:03 -0700528 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700529
Sean McCullough86b56862025-04-18 13:04:03 -0700530 /* Tool call cards */
531 .tool-call-cards-container {
532 display: flex;
533 flex-direction: column;
534 gap: 8px;
535 margin-top: 8px;
536 }
537
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000538 /* Error message specific styling */
539 .error .message-content {
540 background-color: #ffebee;
541 border-left: 3px solid #f44336;
Sean McCullough86b56862025-04-18 13:04:03 -0700542 }
543
544 .end-of-turn {
545 margin-bottom: 15px;
546 }
547
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000548 .end-of-turn-indicator {
549 display: block;
550 font-size: 11px;
551 color: #777;
552 padding: 2px 0;
553 margin-top: 8px;
554 text-align: right;
555 font-style: italic;
556 }
557
558 .user .end-of-turn-indicator {
559 color: rgba(255, 255, 255, 0.7);
560 }
561
562 /* Message info panel styling */
563 .message-info-panel {
564 margin-top: 8px;
565 padding: 8px;
566 background-color: rgba(0, 0, 0, 0.03);
567 border-radius: 6px;
568 font-size: 12px;
569 transition: all 0.2s ease;
570 border-left: 2px solid rgba(0, 0, 0, 0.1);
571 }
572
573 .user .message-info-panel {
574 background-color: rgba(255, 255, 255, 0.15);
575 border-left: 2px solid rgba(255, 255, 255, 0.2);
576 }
577
578 .info-row {
579 margin-bottom: 3px;
580 display: flex;
581 }
582
583 .info-label {
584 font-weight: bold;
585 margin-right: 5px;
586 min-width: 60px;
587 }
588
589 .info-value {
590 flex: 1;
591 }
592
593 .conversation-id {
594 font-family: monospace;
Sean McCullough86b56862025-04-18 13:04:03 -0700595 font-size: 10px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000596 word-break: break-all;
Sean McCullough86b56862025-04-18 13:04:03 -0700597 }
598
599 .markdown-content {
600 box-sizing: border-box;
601 min-width: 200px;
602 margin: 0 auto;
603 }
604
605 .markdown-content p {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000606 margin-block-start: 0.3em;
607 margin-block-end: 0.3em;
608 }
609
610 .markdown-content p:first-child {
611 margin-block-start: 0;
612 }
613
614 .markdown-content p:last-child {
615 margin-block-end: 0;
616 }
617
618 /* Styling for markdown elements */
619 .markdown-content a {
620 color: inherit;
621 text-decoration: underline;
622 }
623
624 .user .markdown-content a {
625 color: #fff;
626 text-decoration: underline;
627 }
628
629 .markdown-content ul,
630 .markdown-content ol {
631 padding-left: 1.5em;
632 margin: 0.5em 0;
633 }
634
635 .markdown-content blockquote {
636 border-left: 3px solid rgba(0, 0, 0, 0.2);
637 padding-left: 1em;
638 margin-left: 0.5em;
639 font-style: italic;
640 }
641
642 .user .markdown-content blockquote {
643 border-left: 3px solid rgba(255, 255, 255, 0.4);
Sean McCullough86b56862025-04-18 13:04:03 -0700644 }
Autoformatterdded2d62025-04-28 00:27:21 +0000645
Sean McCullough8d93e362025-04-27 23:32:18 +0000646 /* Mermaid diagram styling */
647 .mermaid-container {
648 margin: 1em 0;
649 padding: 0.5em;
650 background-color: #f8f8f8;
651 border-radius: 4px;
652 overflow-x: auto;
653 }
Autoformatterdded2d62025-04-28 00:27:21 +0000654
Sean McCullough8d93e362025-04-27 23:32:18 +0000655 .mermaid {
656 text-align: center;
657 }
Sean McCullough86b56862025-04-18 13:04:03 -0700658 `;
659
Sean McCullough8d93e362025-04-27 23:32:18 +0000660 // Track mermaid diagrams that need rendering
661 private mermaidDiagrams = new Map();
662
Sean McCullough86b56862025-04-18 13:04:03 -0700663 constructor() {
664 super();
Sean McCullough8d93e362025-04-27 23:32:18 +0000665 // Initialize mermaid with specific config
666 mermaid.initialize({
667 startOnLoad: false,
Sean McCulloughf98d7302025-04-27 17:44:06 -0700668 suppressErrorRendering: true,
Autoformatterdded2d62025-04-28 00:27:21 +0000669 theme: "default",
670 securityLevel: "loose", // Allows more flexibility but be careful with user-generated content
671 fontFamily: "monospace",
Sean McCullough8d93e362025-04-27 23:32:18 +0000672 });
Sean McCullough86b56862025-04-18 13:04:03 -0700673 }
674
675 // See https://lit.dev/docs/components/lifecycle/
676 connectedCallback() {
677 super.connectedCallback();
678 }
Autoformatterdded2d62025-04-28 00:27:21 +0000679
Sean McCullough8d93e362025-04-27 23:32:18 +0000680 // After the component is updated and rendered, render any mermaid diagrams
681 updated(changedProperties: Map<string, unknown>) {
682 super.updated(changedProperties);
683 this.renderMermaidDiagrams();
Pokey Rulea10f1512025-05-15 13:53:26 +0000684 this.setupCodeBlockCopyButtons();
Sean McCullough8d93e362025-04-27 23:32:18 +0000685 }
Autoformatterdded2d62025-04-28 00:27:21 +0000686
Sean McCullough8d93e362025-04-27 23:32:18 +0000687 // Render mermaid diagrams after the component is updated
688 renderMermaidDiagrams() {
689 // Add a small delay to ensure the DOM is fully rendered
690 setTimeout(() => {
691 // Find all mermaid containers in our shadow root
Autoformatterdded2d62025-04-28 00:27:21 +0000692 const containers = this.shadowRoot?.querySelectorAll(".mermaid");
Sean McCullough8d93e362025-04-27 23:32:18 +0000693 if (!containers || containers.length === 0) return;
Autoformatterdded2d62025-04-28 00:27:21 +0000694
Sean McCullough8d93e362025-04-27 23:32:18 +0000695 // Process each mermaid diagram
Autoformatterdded2d62025-04-28 00:27:21 +0000696 containers.forEach((container) => {
Sean McCullough8d93e362025-04-27 23:32:18 +0000697 const id = container.id;
Autoformatterdded2d62025-04-28 00:27:21 +0000698 const code = container.textContent || "";
Sean McCullough8d93e362025-04-27 23:32:18 +0000699 if (!code || !id) return; // Use return for forEach instead of continue
Autoformatterdded2d62025-04-28 00:27:21 +0000700
Sean McCullough8d93e362025-04-27 23:32:18 +0000701 try {
702 // Clear any previous content
703 container.innerHTML = code;
Autoformatterdded2d62025-04-28 00:27:21 +0000704
Sean McCullough8d93e362025-04-27 23:32:18 +0000705 // Render the mermaid diagram using promise
Autoformatterdded2d62025-04-28 00:27:21 +0000706 mermaid
707 .render(`${id}-svg`, code)
Sean McCullough8d93e362025-04-27 23:32:18 +0000708 .then(({ svg }) => {
709 container.innerHTML = svg;
710 })
Autoformatterdded2d62025-04-28 00:27:21 +0000711 .catch((err) => {
712 console.error("Error rendering 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 } catch (err) {
Autoformatterdded2d62025-04-28 00:27:21 +0000717 console.error("Error processing mermaid diagram:", err);
Sean McCullough8d93e362025-04-27 23:32:18 +0000718 // Show the original code as fallback
719 container.innerHTML = `<pre>${code}</pre>`;
720 }
721 });
722 }, 100); // Small delay to ensure DOM is ready
723 }
Sean McCullough86b56862025-04-18 13:04:03 -0700724
Pokey Rulea10f1512025-05-15 13:53:26 +0000725 // Setup code block copy buttons after component is updated
726 setupCodeBlockCopyButtons() {
727 setTimeout(() => {
728 // Find all copy buttons in code blocks
729 const copyButtons =
730 this.shadowRoot?.querySelectorAll(".code-copy-button");
731 if (!copyButtons || copyButtons.length === 0) return;
732
733 // Add click event listener to each button
734 copyButtons.forEach((button) => {
735 button.addEventListener("click", (e) => {
736 e.stopPropagation();
737 const codeId = (button as HTMLElement).dataset.codeId;
738 if (!codeId) return;
739
740 const codeElement = this.shadowRoot?.querySelector(`#${codeId}`);
741 if (!codeElement) return;
742
743 const codeText = codeElement.textContent || "";
744 const buttonRect = button.getBoundingClientRect();
745
746 // Copy code to clipboard
747 navigator.clipboard
748 .writeText(codeText)
749 .then(() => {
750 // Show success indicator
751 const originalHTML = button.innerHTML;
752 button.innerHTML = `
753 <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">
754 <path d="M20 6L9 17l-5-5"></path>
755 </svg>
756 `;
757
758 // Display floating message
759 this.showFloatingMessage("Copied!", buttonRect, "success");
760
761 // Reset button after delay
762 setTimeout(() => {
763 button.innerHTML = originalHTML;
764 }, 2000);
765 })
766 .catch((err) => {
767 console.error("Failed to copy code:", err);
768 this.showFloatingMessage("Failed to copy!", buttonRect, "error");
769 });
770 });
771 });
772 }, 100); // Small delay to ensure DOM is ready
773 }
774
Sean McCullough86b56862025-04-18 13:04:03 -0700775 // See https://lit.dev/docs/components/lifecycle/
776 disconnectedCallback() {
777 super.disconnectedCallback();
778 }
779
780 renderMarkdown(markdownContent: string): string {
781 try {
Sean McCullough8d93e362025-04-27 23:32:18 +0000782 // Create a custom renderer
783 const renderer = new Renderer();
784 const originalCodeRenderer = renderer.code.bind(renderer);
Autoformatterdded2d62025-04-28 00:27:21 +0000785
Pokey Rulea10f1512025-05-15 13:53:26 +0000786 // Override the code renderer to handle mermaid diagrams and add copy buttons
Autoformatterdded2d62025-04-28 00:27:21 +0000787 renderer.code = function ({ text, lang, escaped }: Tokens.Code): string {
788 if (lang === "mermaid") {
Sean McCullough8d93e362025-04-27 23:32:18 +0000789 // Generate a unique ID for this diagram
790 const id = `mermaid-diagram-${Math.random().toString(36).substring(2, 10)}`;
Autoformatterdded2d62025-04-28 00:27:21 +0000791
Sean McCullough8d93e362025-04-27 23:32:18 +0000792 // Just create the container and mermaid div - we'll render it in the updated() lifecycle method
793 return `<div class="mermaid-container">
794 <div class="mermaid" id="${id}">${text}</div>
795 </div>`;
796 }
Pokey Rulea10f1512025-05-15 13:53:26 +0000797
Philip Zeyliger0d092842025-06-09 18:57:12 -0700798 // For regular code blocks, call the original renderer to get properly escaped HTML
799 const originalCodeHtml = originalCodeRenderer({ text, lang, escaped });
800
801 // Extract the code content from the original HTML to add our custom wrapper
802 // The original renderer returns: <pre><code class="language-x">escapedText</code></pre>
803 const codeMatch = originalCodeHtml.match(
804 /<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/,
805 );
806 if (!codeMatch) {
807 // Fallback to original if we can't parse it
808 return originalCodeHtml;
809 }
810
811 const escapedText = codeMatch[1];
Pokey Rulea10f1512025-05-15 13:53:26 +0000812 const id = `code-block-${Math.random().toString(36).substring(2, 10)}`;
813 const langClass = lang ? ` class="language-${lang}"` : "";
814
815 return `<div class="code-block-container">
816 <div class="code-block-header">
817 ${lang ? `<span class="code-language">${lang}</span>` : ""}
818 <button class="code-copy-button" title="Copy code" data-code-id="${id}">
819 <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">
820 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
821 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
822 </svg>
823 </button>
824 </div>
Philip Zeyliger0d092842025-06-09 18:57:12 -0700825 <pre><code id="${id}"${langClass}>${escapedText}</code></pre>
Pokey Rulea10f1512025-05-15 13:53:26 +0000826 </div>`;
Sean McCullough8d93e362025-04-27 23:32:18 +0000827 };
Autoformatterdded2d62025-04-28 00:27:21 +0000828
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000829 // Set markdown options for proper code block highlighting
Sean McCullough86b56862025-04-18 13:04:03 -0700830 const markedOptions: MarkedOptions = {
831 gfm: true, // GitHub Flavored Markdown
832 breaks: true, // Convert newlines to <br>
833 async: false,
Autoformatterdded2d62025-04-28 00:27:21 +0000834 renderer: renderer,
Sean McCullough86b56862025-04-18 13:04:03 -0700835 };
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000836
837 // Parse markdown and sanitize the output HTML with DOMPurify
838 const htmlOutput = marked.parse(markdownContent, markedOptions) as string;
839 return DOMPurify.sanitize(htmlOutput, {
840 // Allow common HTML elements that are safe
841 ALLOWED_TAGS: [
842 "p",
843 "br",
844 "strong",
845 "em",
846 "b",
847 "i",
848 "u",
849 "s",
850 "code",
851 "pre",
852 "h1",
853 "h2",
854 "h3",
855 "h4",
856 "h5",
857 "h6",
858 "ul",
859 "ol",
860 "li",
861 "blockquote",
862 "a",
863 "div",
864 "span", // For mermaid diagrams and code blocks
865 "svg",
866 "g",
867 "path",
868 "rect",
869 "circle",
870 "text",
871 "line",
872 "polygon", // For mermaid SVG
873 "button", // For code copy buttons
874 ],
875 ALLOWED_ATTR: [
876 "href",
877 "title",
878 "target",
879 "rel", // For links
880 "class",
881 "id", // For styling and functionality
882 "data-*", // For code copy buttons
883 // SVG attributes for mermaid diagrams
884 "viewBox",
885 "width",
886 "height",
887 "xmlns",
888 "fill",
889 "stroke",
890 "stroke-width",
891 "d",
892 "x",
893 "y",
894 "x1",
895 "y1",
896 "x2",
897 "y2",
898 "cx",
899 "cy",
900 "r",
901 "rx",
902 "ry",
903 "points",
904 "transform",
905 "text-anchor",
906 "font-size",
907 "font-family",
908 ],
909 // Allow data attributes for functionality
910 ALLOW_DATA_ATTR: true,
911 // Keep whitespace for code formatting
912 KEEP_CONTENT: true,
913 });
Sean McCullough86b56862025-04-18 13:04:03 -0700914 } catch (error) {
915 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000916 // Fallback to sanitized plain text if markdown parsing fails
917 return DOMPurify.sanitize(markdownContent);
Sean McCullough86b56862025-04-18 13:04:03 -0700918 }
919 }
920
921 /**
922 * Format timestamp for display
923 */
924 formatTimestamp(
925 timestamp: string | number | Date | null | undefined,
926 defaultValue: string = "",
927 ): string {
928 if (!timestamp) return defaultValue;
929 try {
930 const date = new Date(timestamp);
931 if (isNaN(date.getTime())) return defaultValue;
932
933 // Format: Mar 13, 2025 09:53:25 AM
934 return date.toLocaleString("en-US", {
935 month: "short",
936 day: "numeric",
937 year: "numeric",
938 hour: "numeric",
939 minute: "2-digit",
940 second: "2-digit",
941 hour12: true,
942 });
943 } catch (e) {
944 return defaultValue;
945 }
946 }
947
948 formatNumber(
949 num: number | null | undefined,
950 defaultValue: string = "0",
951 ): string {
952 if (num === undefined || num === null) return defaultValue;
953 try {
954 return num.toLocaleString();
955 } catch (e) {
956 return String(num);
957 }
958 }
959 formatCurrency(
960 num: number | string | null | undefined,
961 defaultValue: string = "$0.00",
962 isMessageLevel: boolean = false,
963 ): string {
964 if (num === undefined || num === null) return defaultValue;
965 try {
966 // Use 4 decimal places for message-level costs, 2 for totals
967 const decimalPlaces = isMessageLevel ? 4 : 2;
968 return `$${parseFloat(String(num)).toFixed(decimalPlaces)}`;
969 } catch (e) {
970 return defaultValue;
971 }
972 }
973
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000974 // Format duration from nanoseconds to a human-readable string
975 _formatDuration(nanoseconds: number | null | undefined): string {
976 if (!nanoseconds) return "0s";
977
978 const seconds = nanoseconds / 1e9;
979
980 if (seconds < 60) {
981 return `${seconds.toFixed(1)}s`;
982 } else if (seconds < 3600) {
983 const minutes = Math.floor(seconds / 60);
984 const remainingSeconds = seconds % 60;
985 return `${minutes}min ${remainingSeconds.toFixed(0)}s`;
986 } else {
987 const hours = Math.floor(seconds / 3600);
988 const remainingSeconds = seconds % 3600;
989 const minutes = Math.floor(remainingSeconds / 60);
990 return `${hours}h ${minutes}min`;
991 }
992 }
993
Sean McCullough86b56862025-04-18 13:04:03 -0700994 showCommit(commitHash: string) {
Sean McCullough71941bd2025-04-18 13:31:48 -0700995 this.dispatchEvent(
996 new CustomEvent("show-commit-diff", {
997 bubbles: true,
998 composed: true,
999 detail: { commitHash },
1000 }),
1001 );
Sean McCullough86b56862025-04-18 13:04:03 -07001002 }
1003
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001004 _toggleInfo(e: Event) {
1005 e.stopPropagation();
1006 this.showInfo = !this.showInfo;
1007 }
1008
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001009 copyToClipboard(text: string, event: Event) {
1010 const element = event.currentTarget as HTMLElement;
1011 const rect = element.getBoundingClientRect();
1012
1013 navigator.clipboard
1014 .writeText(text)
1015 .then(() => {
1016 this.showFloatingMessage("Copied!", rect, "success");
1017 })
1018 .catch((err) => {
1019 console.error("Failed to copy text: ", err);
1020 this.showFloatingMessage("Failed to copy!", rect, "error");
1021 });
1022 }
1023
1024 showFloatingMessage(
1025 message: string,
1026 targetRect: DOMRect,
1027 type: "success" | "error",
1028 ) {
1029 // Create floating message element
1030 const floatingMsg = document.createElement("div");
1031 floatingMsg.textContent = message;
1032 floatingMsg.className = `floating-message ${type}`;
1033
1034 // Position it near the clicked element
1035 // Position just above the element
1036 const top = targetRect.top - 30;
1037 const left = targetRect.left + targetRect.width / 2 - 40;
1038
1039 floatingMsg.style.position = "fixed";
1040 floatingMsg.style.top = `${top}px`;
1041 floatingMsg.style.left = `${left}px`;
1042 floatingMsg.style.zIndex = "9999";
1043
1044 // Add to document body
1045 document.body.appendChild(floatingMsg);
1046
1047 // Animate in
1048 floatingMsg.style.opacity = "0";
1049 floatingMsg.style.transform = "translateY(10px)";
1050
1051 setTimeout(() => {
1052 floatingMsg.style.opacity = "1";
1053 floatingMsg.style.transform = "translateY(0)";
1054 }, 10);
1055
1056 // Remove after animation
1057 setTimeout(() => {
1058 floatingMsg.style.opacity = "0";
1059 floatingMsg.style.transform = "translateY(-10px)";
1060
1061 setTimeout(() => {
1062 document.body.removeChild(floatingMsg);
1063 }, 300);
1064 }, 1500);
1065 }
1066
Sean McCullough86b56862025-04-18 13:04:03 -07001067 render() {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001068 // Calculate if this is an end of turn message with no parent conversation ID
1069 const isEndOfTurn =
1070 this.message?.end_of_turn && !this.message?.parent_conversation_id;
1071
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001072 const isPreCompaction =
1073 this.message?.idx !== undefined &&
1074 this.message.idx < this.firstMessageIndex;
1075
Sean McCullough86b56862025-04-18 13:04:03 -07001076 return html`
1077 <div
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001078 class="message ${this.message?.type} ${isEndOfTurn
Sean McCullough86b56862025-04-18 13:04:03 -07001079 ? "end-of-turn"
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001080 : ""} ${isPreCompaction ? "pre-compaction" : ""}"
Sean McCullough86b56862025-04-18 13:04:03 -07001081 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001082 <div class="message-container">
1083 <!-- Left area (empty for simplicity) -->
1084 <div class="message-metadata-left"></div>
1085
1086 <!-- Message bubble -->
1087 <div class="message-bubble-container">
1088 <div class="message-content">
1089 <div class="message-text-container">
1090 <div class="message-actions">
1091 ${copyButton(this.message?.content)}
1092 <button
1093 class="info-icon"
1094 title="Show message details"
1095 @click=${this._toggleInfo}
Sean McCullough71941bd2025-04-18 13:31:48 -07001096 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001097 <svg
1098 xmlns="http://www.w3.org/2000/svg"
1099 width="16"
1100 height="16"
1101 viewBox="0 0 24 24"
1102 fill="none"
1103 stroke="currentColor"
1104 stroke-width="2"
1105 stroke-linecap="round"
1106 stroke-linejoin="round"
1107 >
1108 <circle cx="12" cy="12" r="10"></circle>
1109 <line x1="12" y1="16" x2="12" y2="12"></line>
1110 <line x1="12" y1="8" x2="12.01" y2="8"></line>
1111 </svg>
1112 </button>
1113 </div>
1114 ${this.message?.content
1115 ? html`
1116 <div class="message-text markdown-content">
1117 ${unsafeHTML(
1118 this.renderMarkdown(this.message?.content),
1119 )}
1120 </div>
1121 `
1122 : ""}
1123
1124 <!-- End of turn indicator inside the bubble -->
1125 ${isEndOfTurn && this.message?.elapsed
1126 ? html`
1127 <div class="end-of-turn-indicator">
1128 end of turn
1129 (${this._formatDuration(this.message?.elapsed)})
1130 </div>
1131 `
1132 : ""}
1133
1134 <!-- Info panel that can be toggled -->
1135 ${this.showInfo
1136 ? html`
1137 <div class="message-info-panel">
1138 <div class="info-row">
1139 <span class="info-label">Type:</span>
1140 <span class="info-value">${this.message?.type}</span>
1141 </div>
1142 <div class="info-row">
1143 <span class="info-label">Time:</span>
1144 <span class="info-value"
1145 >${this.formatTimestamp(
1146 this.message?.timestamp,
1147 "",
1148 )}</span
1149 >
1150 </div>
1151 ${this.message?.elapsed
1152 ? html`
1153 <div class="info-row">
1154 <span class="info-label">Duration:</span>
1155 <span class="info-value"
1156 >${this._formatDuration(
1157 this.message?.elapsed,
1158 )}</span
1159 >
1160 </div>
1161 `
1162 : ""}
1163 ${this.message?.usage
1164 ? html`
1165 <div class="info-row">
1166 <span class="info-label">Tokens:</span>
1167 <span class="info-value">
1168 ${this.message?.usage
1169 ? html`
1170 <div>
1171 Input:
1172 ${this.formatNumber(
1173 this.message?.usage?.input_tokens ||
1174 0,
1175 )}
1176 </div>
1177 ${this.message?.usage
1178 ?.cache_creation_input_tokens
1179 ? html`
1180 <div>
1181 Cache creation:
1182 ${this.formatNumber(
1183 this.message?.usage
1184 ?.cache_creation_input_tokens,
1185 )}
1186 </div>
1187 `
1188 : ""}
1189 ${this.message?.usage
1190 ?.cache_read_input_tokens
1191 ? html`
1192 <div>
1193 Cache read:
1194 ${this.formatNumber(
1195 this.message?.usage
1196 ?.cache_read_input_tokens,
1197 )}
1198 </div>
1199 `
1200 : ""}
1201 <div>
1202 Output:
1203 ${this.formatNumber(
1204 this.message?.usage?.output_tokens,
1205 )}
1206 </div>
1207 <div>
1208 Cost:
1209 ${this.formatCurrency(
1210 this.message?.usage?.cost_usd,
1211 )}
1212 </div>
1213 `
1214 : "N/A"}
1215 </span>
1216 </div>
1217 `
1218 : ""}
1219 ${this.message?.conversation_id
1220 ? html`
1221 <div class="info-row">
1222 <span class="info-label">Conversation ID:</span>
1223 <span class="info-value conversation-id"
1224 >${this.message?.conversation_id}</span
1225 >
1226 </div>
1227 `
1228 : ""}
1229 </div>
1230 `
1231 : ""}
1232 </div>
1233
1234 <!-- Tool calls - only shown for agent messages -->
1235 ${this.message?.type === "agent"
1236 ? html`
1237 <sketch-tool-calls
1238 .toolCalls=${this.message?.tool_calls}
1239 .open=${this.open}
1240 ></sketch-tool-calls>
1241 `
1242 : ""}
1243
1244 <!-- Commits section (redesigned as bubbles) -->
1245 ${this.message?.commits
1246 ? html`
1247 <div class="commits-container">
1248 <div class="commit-notification">
1249 ${this.message.commits.length} new
1250 commit${this.message.commits.length > 1 ? "s" : ""}
1251 detected
1252 </div>
1253 ${this.message.commits.map((commit) => {
1254 return html`
1255 <div class="commit-card">
Philip Zeyliger72682df2025-04-23 13:09:46 -07001256 <span
1257 class="commit-hash"
1258 title="Click to copy: ${commit.hash}"
1259 @click=${(e) =>
1260 this.copyToClipboard(
1261 commit.hash.substring(0, 8),
1262 e,
1263 )}
1264 >
Pokey Rule7be879f2025-04-23 15:30:15 +01001265 ${commit.hash.substring(0, 8)}
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001266 </span>
1267 ${commit.pushed_branch
1268 ? html`
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001269 <span
1270 class="commit-branch pushed-branch"
1271 title="Click to copy: ${commit.pushed_branch}"
1272 @click=${(e) =>
1273 this.copyToClipboard(
1274 commit.pushed_branch,
1275 e,
1276 )}
1277 >${commit.pushed_branch}</span
1278 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001279 `
1280 : ``}
1281 <span class="commit-subject"
1282 >${commit.subject}</span
Sean McCullough71941bd2025-04-18 13:31:48 -07001283 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001284 <button
1285 class="commit-diff-button"
1286 @click=${() => this.showCommit(commit.hash)}
1287 >
1288 View Diff
1289 </button>
Sean McCullough86b56862025-04-18 13:04:03 -07001290 </div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001291 `;
1292 })}
1293 </div>
1294 `
1295 : ""}
1296 </div>
1297 </div>
1298
1299 <!-- Right side (empty for consistency) -->
1300 <div class="message-metadata-right"></div>
Sean McCullough86b56862025-04-18 13:04:03 -07001301 </div>
1302 </div>
1303 `;
1304 }
1305}
1306
Sean McCullough71941bd2025-04-18 13:31:48 -07001307function copyButton(textToCopy: string) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001308 // Use an icon of overlapping rectangles for copy
1309 const buttonClass = "copy-icon";
1310
1311 // SVG for copy icon (two overlapping rectangles)
1312 const copyIcon = html`<svg
1313 xmlns="http://www.w3.org/2000/svg"
1314 width="16"
1315 height="16"
1316 viewBox="0 0 24 24"
1317 fill="none"
1318 stroke="currentColor"
1319 stroke-width="2"
1320 stroke-linecap="round"
1321 stroke-linejoin="round"
1322 >
1323 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1324 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1325 </svg>`;
1326
1327 // SVG for success check mark
1328 const successIcon = html`<svg
1329 xmlns="http://www.w3.org/2000/svg"
1330 width="16"
1331 height="16"
1332 viewBox="0 0 24 24"
1333 fill="none"
1334 stroke="currentColor"
1335 stroke-width="2"
1336 stroke-linecap="round"
1337 stroke-linejoin="round"
1338 >
1339 <path d="M20 6L9 17l-5-5"></path>
1340 </svg>`;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001341
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001342 const ret = html`<button
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001343 class="${buttonClass}"
1344 title="Copy to clipboard"
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001345 @click=${(e: Event) => {
1346 e.stopPropagation();
1347 const copyButton = e.currentTarget as HTMLButtonElement;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001348 const originalInnerHTML = copyButton.innerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001349 navigator.clipboard
1350 .writeText(textToCopy)
1351 .then(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001352 copyButton.innerHTML = "";
1353 const successElement = document.createElement("div");
1354 copyButton.appendChild(successElement);
1355 render(successIcon, successElement);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001356 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001357 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001358 }, 2000);
1359 })
1360 .catch((err) => {
1361 console.error("Failed to copy text: ", err);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001362 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001363 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001364 }, 2000);
1365 });
1366 }}
1367 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001368 ${copyIcon}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001369 </button>`;
Sean McCullough86b56862025-04-18 13:04:03 -07001370
Sean McCullough71941bd2025-04-18 13:31:48 -07001371 return ret;
Sean McCullough86b56862025-04-18 13:04:03 -07001372}
1373
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001374// Create global styles for floating messages
1375const floatingMessageStyles = document.createElement("style");
1376floatingMessageStyles.textContent = `
1377 .floating-message {
1378 background-color: rgba(0, 0, 0, 0.8);
1379 color: white;
1380 padding: 5px 10px;
1381 border-radius: 4px;
1382 font-size: 12px;
1383 font-family: system-ui, sans-serif;
1384 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
1385 pointer-events: none;
1386 transition: opacity 0.3s ease, transform 0.3s ease;
1387 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001388
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001389 .floating-message.success {
1390 background-color: rgba(40, 167, 69, 0.9);
1391 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001392
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001393 .floating-message.error {
1394 background-color: rgba(220, 53, 69, 0.9);
1395 }
Philip Zeyligere31d2a92025-05-11 15:22:35 -07001396
1397 /* Style for code, pre elements, and tool components to ensure proper wrapping/truncation */
1398 pre, code, sketch-tool-calls, sketch-tool-card, sketch-tool-card-bash {
1399 white-space: nowrap;
1400 overflow: hidden;
1401 text-overflow: ellipsis;
1402 max-width: 100%;
1403 }
1404
1405 /* Special rule for the message content container */
1406 .message-content {
1407 max-width: 100% !important;
1408 overflow: hidden !important;
1409 }
1410
1411 /* Ensure tool call containers don't overflow */
1412 ::slotted(sketch-tool-calls) {
1413 max-width: 100%;
1414 width: 100%;
1415 overflow-wrap: break-word;
1416 word-break: break-word;
1417 }
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001418`;
1419document.head.appendChild(floatingMessageStyles);
1420
Sean McCullough86b56862025-04-18 13:04:03 -07001421declare global {
1422 interface HTMLElementTagNameMap {
1423 "sketch-timeline-message": SketchTimelineMessage;
1424 }
1425}