blob: 3d10b07c35cca52a615a5f8aaf79e3d9f742af28 [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
793 // For regular code blocks, add a copy button
794 const id = `code-block-${Math.random().toString(36).substring(2, 10)}`;
795 const langClass = lang ? ` class="language-${lang}"` : "";
796
797 return `<div class="code-block-container">
798 <div class="code-block-header">
799 ${lang ? `<span class="code-language">${lang}</span>` : ""}
800 <button class="code-copy-button" title="Copy code" data-code-id="${id}">
801 <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">
802 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
803 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
804 </svg>
805 </button>
806 </div>
807 <pre><code id="${id}"${langClass}>${text}</code></pre>
808 </div>`;
Sean McCullough8d93e362025-04-27 23:32:18 +0000809 };
Autoformatterdded2d62025-04-28 00:27:21 +0000810
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000811 // Set markdown options for proper code block highlighting
Sean McCullough86b56862025-04-18 13:04:03 -0700812 const markedOptions: MarkedOptions = {
813 gfm: true, // GitHub Flavored Markdown
814 breaks: true, // Convert newlines to <br>
815 async: false,
Autoformatterdded2d62025-04-28 00:27:21 +0000816 renderer: renderer,
Sean McCullough86b56862025-04-18 13:04:03 -0700817 };
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000818
819 // Parse markdown and sanitize the output HTML with DOMPurify
820 const htmlOutput = marked.parse(markdownContent, markedOptions) as string;
821 return DOMPurify.sanitize(htmlOutput, {
822 // Allow common HTML elements that are safe
823 ALLOWED_TAGS: [
824 "p",
825 "br",
826 "strong",
827 "em",
828 "b",
829 "i",
830 "u",
831 "s",
832 "code",
833 "pre",
834 "h1",
835 "h2",
836 "h3",
837 "h4",
838 "h5",
839 "h6",
840 "ul",
841 "ol",
842 "li",
843 "blockquote",
844 "a",
845 "div",
846 "span", // For mermaid diagrams and code blocks
847 "svg",
848 "g",
849 "path",
850 "rect",
851 "circle",
852 "text",
853 "line",
854 "polygon", // For mermaid SVG
855 "button", // For code copy buttons
856 ],
857 ALLOWED_ATTR: [
858 "href",
859 "title",
860 "target",
861 "rel", // For links
862 "class",
863 "id", // For styling and functionality
864 "data-*", // For code copy buttons
865 // SVG attributes for mermaid diagrams
866 "viewBox",
867 "width",
868 "height",
869 "xmlns",
870 "fill",
871 "stroke",
872 "stroke-width",
873 "d",
874 "x",
875 "y",
876 "x1",
877 "y1",
878 "x2",
879 "y2",
880 "cx",
881 "cy",
882 "r",
883 "rx",
884 "ry",
885 "points",
886 "transform",
887 "text-anchor",
888 "font-size",
889 "font-family",
890 ],
891 // Allow data attributes for functionality
892 ALLOW_DATA_ATTR: true,
893 // Keep whitespace for code formatting
894 KEEP_CONTENT: true,
895 });
Sean McCullough86b56862025-04-18 13:04:03 -0700896 } catch (error) {
897 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000898 // Fallback to sanitized plain text if markdown parsing fails
899 return DOMPurify.sanitize(markdownContent);
Sean McCullough86b56862025-04-18 13:04:03 -0700900 }
901 }
902
903 /**
904 * Format timestamp for display
905 */
906 formatTimestamp(
907 timestamp: string | number | Date | null | undefined,
908 defaultValue: string = "",
909 ): string {
910 if (!timestamp) return defaultValue;
911 try {
912 const date = new Date(timestamp);
913 if (isNaN(date.getTime())) return defaultValue;
914
915 // Format: Mar 13, 2025 09:53:25 AM
916 return date.toLocaleString("en-US", {
917 month: "short",
918 day: "numeric",
919 year: "numeric",
920 hour: "numeric",
921 minute: "2-digit",
922 second: "2-digit",
923 hour12: true,
924 });
925 } catch (e) {
926 return defaultValue;
927 }
928 }
929
930 formatNumber(
931 num: number | null | undefined,
932 defaultValue: string = "0",
933 ): string {
934 if (num === undefined || num === null) return defaultValue;
935 try {
936 return num.toLocaleString();
937 } catch (e) {
938 return String(num);
939 }
940 }
941 formatCurrency(
942 num: number | string | null | undefined,
943 defaultValue: string = "$0.00",
944 isMessageLevel: boolean = false,
945 ): string {
946 if (num === undefined || num === null) return defaultValue;
947 try {
948 // Use 4 decimal places for message-level costs, 2 for totals
949 const decimalPlaces = isMessageLevel ? 4 : 2;
950 return `$${parseFloat(String(num)).toFixed(decimalPlaces)}`;
951 } catch (e) {
952 return defaultValue;
953 }
954 }
955
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000956 // Format duration from nanoseconds to a human-readable string
957 _formatDuration(nanoseconds: number | null | undefined): string {
958 if (!nanoseconds) return "0s";
959
960 const seconds = nanoseconds / 1e9;
961
962 if (seconds < 60) {
963 return `${seconds.toFixed(1)}s`;
964 } else if (seconds < 3600) {
965 const minutes = Math.floor(seconds / 60);
966 const remainingSeconds = seconds % 60;
967 return `${minutes}min ${remainingSeconds.toFixed(0)}s`;
968 } else {
969 const hours = Math.floor(seconds / 3600);
970 const remainingSeconds = seconds % 3600;
971 const minutes = Math.floor(remainingSeconds / 60);
972 return `${hours}h ${minutes}min`;
973 }
974 }
975
Sean McCullough86b56862025-04-18 13:04:03 -0700976 showCommit(commitHash: string) {
Sean McCullough71941bd2025-04-18 13:31:48 -0700977 this.dispatchEvent(
978 new CustomEvent("show-commit-diff", {
979 bubbles: true,
980 composed: true,
981 detail: { commitHash },
982 }),
983 );
Sean McCullough86b56862025-04-18 13:04:03 -0700984 }
985
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000986 _toggleInfo(e: Event) {
987 e.stopPropagation();
988 this.showInfo = !this.showInfo;
989 }
990
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000991 copyToClipboard(text: string, event: Event) {
992 const element = event.currentTarget as HTMLElement;
993 const rect = element.getBoundingClientRect();
994
995 navigator.clipboard
996 .writeText(text)
997 .then(() => {
998 this.showFloatingMessage("Copied!", rect, "success");
999 })
1000 .catch((err) => {
1001 console.error("Failed to copy text: ", err);
1002 this.showFloatingMessage("Failed to copy!", rect, "error");
1003 });
1004 }
1005
1006 showFloatingMessage(
1007 message: string,
1008 targetRect: DOMRect,
1009 type: "success" | "error",
1010 ) {
1011 // Create floating message element
1012 const floatingMsg = document.createElement("div");
1013 floatingMsg.textContent = message;
1014 floatingMsg.className = `floating-message ${type}`;
1015
1016 // Position it near the clicked element
1017 // Position just above the element
1018 const top = targetRect.top - 30;
1019 const left = targetRect.left + targetRect.width / 2 - 40;
1020
1021 floatingMsg.style.position = "fixed";
1022 floatingMsg.style.top = `${top}px`;
1023 floatingMsg.style.left = `${left}px`;
1024 floatingMsg.style.zIndex = "9999";
1025
1026 // Add to document body
1027 document.body.appendChild(floatingMsg);
1028
1029 // Animate in
1030 floatingMsg.style.opacity = "0";
1031 floatingMsg.style.transform = "translateY(10px)";
1032
1033 setTimeout(() => {
1034 floatingMsg.style.opacity = "1";
1035 floatingMsg.style.transform = "translateY(0)";
1036 }, 10);
1037
1038 // Remove after animation
1039 setTimeout(() => {
1040 floatingMsg.style.opacity = "0";
1041 floatingMsg.style.transform = "translateY(-10px)";
1042
1043 setTimeout(() => {
1044 document.body.removeChild(floatingMsg);
1045 }, 300);
1046 }, 1500);
1047 }
1048
Sean McCullough86b56862025-04-18 13:04:03 -07001049 render() {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001050 // Calculate if this is an end of turn message with no parent conversation ID
1051 const isEndOfTurn =
1052 this.message?.end_of_turn && !this.message?.parent_conversation_id;
1053
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001054 const isPreCompaction =
1055 this.message?.idx !== undefined &&
1056 this.message.idx < this.firstMessageIndex;
1057
Sean McCullough86b56862025-04-18 13:04:03 -07001058 return html`
1059 <div
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001060 class="message ${this.message?.type} ${isEndOfTurn
Sean McCullough86b56862025-04-18 13:04:03 -07001061 ? "end-of-turn"
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001062 : ""} ${isPreCompaction ? "pre-compaction" : ""}"
Sean McCullough86b56862025-04-18 13:04:03 -07001063 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001064 <div class="message-container">
1065 <!-- Left area (empty for simplicity) -->
1066 <div class="message-metadata-left"></div>
1067
1068 <!-- Message bubble -->
1069 <div class="message-bubble-container">
1070 <div class="message-content">
1071 <div class="message-text-container">
1072 <div class="message-actions">
1073 ${copyButton(this.message?.content)}
1074 <button
1075 class="info-icon"
1076 title="Show message details"
1077 @click=${this._toggleInfo}
Sean McCullough71941bd2025-04-18 13:31:48 -07001078 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001079 <svg
1080 xmlns="http://www.w3.org/2000/svg"
1081 width="16"
1082 height="16"
1083 viewBox="0 0 24 24"
1084 fill="none"
1085 stroke="currentColor"
1086 stroke-width="2"
1087 stroke-linecap="round"
1088 stroke-linejoin="round"
1089 >
1090 <circle cx="12" cy="12" r="10"></circle>
1091 <line x1="12" y1="16" x2="12" y2="12"></line>
1092 <line x1="12" y1="8" x2="12.01" y2="8"></line>
1093 </svg>
1094 </button>
1095 </div>
1096 ${this.message?.content
1097 ? html`
1098 <div class="message-text markdown-content">
1099 ${unsafeHTML(
1100 this.renderMarkdown(this.message?.content),
1101 )}
1102 </div>
1103 `
1104 : ""}
1105
1106 <!-- End of turn indicator inside the bubble -->
1107 ${isEndOfTurn && this.message?.elapsed
1108 ? html`
1109 <div class="end-of-turn-indicator">
1110 end of turn
1111 (${this._formatDuration(this.message?.elapsed)})
1112 </div>
1113 `
1114 : ""}
1115
1116 <!-- Info panel that can be toggled -->
1117 ${this.showInfo
1118 ? html`
1119 <div class="message-info-panel">
1120 <div class="info-row">
1121 <span class="info-label">Type:</span>
1122 <span class="info-value">${this.message?.type}</span>
1123 </div>
1124 <div class="info-row">
1125 <span class="info-label">Time:</span>
1126 <span class="info-value"
1127 >${this.formatTimestamp(
1128 this.message?.timestamp,
1129 "",
1130 )}</span
1131 >
1132 </div>
1133 ${this.message?.elapsed
1134 ? html`
1135 <div class="info-row">
1136 <span class="info-label">Duration:</span>
1137 <span class="info-value"
1138 >${this._formatDuration(
1139 this.message?.elapsed,
1140 )}</span
1141 >
1142 </div>
1143 `
1144 : ""}
1145 ${this.message?.usage
1146 ? html`
1147 <div class="info-row">
1148 <span class="info-label">Tokens:</span>
1149 <span class="info-value">
1150 ${this.message?.usage
1151 ? html`
1152 <div>
1153 Input:
1154 ${this.formatNumber(
1155 this.message?.usage?.input_tokens ||
1156 0,
1157 )}
1158 </div>
1159 ${this.message?.usage
1160 ?.cache_creation_input_tokens
1161 ? html`
1162 <div>
1163 Cache creation:
1164 ${this.formatNumber(
1165 this.message?.usage
1166 ?.cache_creation_input_tokens,
1167 )}
1168 </div>
1169 `
1170 : ""}
1171 ${this.message?.usage
1172 ?.cache_read_input_tokens
1173 ? html`
1174 <div>
1175 Cache read:
1176 ${this.formatNumber(
1177 this.message?.usage
1178 ?.cache_read_input_tokens,
1179 )}
1180 </div>
1181 `
1182 : ""}
1183 <div>
1184 Output:
1185 ${this.formatNumber(
1186 this.message?.usage?.output_tokens,
1187 )}
1188 </div>
1189 <div>
1190 Cost:
1191 ${this.formatCurrency(
1192 this.message?.usage?.cost_usd,
1193 )}
1194 </div>
1195 `
1196 : "N/A"}
1197 </span>
1198 </div>
1199 `
1200 : ""}
1201 ${this.message?.conversation_id
1202 ? html`
1203 <div class="info-row">
1204 <span class="info-label">Conversation ID:</span>
1205 <span class="info-value conversation-id"
1206 >${this.message?.conversation_id}</span
1207 >
1208 </div>
1209 `
1210 : ""}
1211 </div>
1212 `
1213 : ""}
1214 </div>
1215
1216 <!-- Tool calls - only shown for agent messages -->
1217 ${this.message?.type === "agent"
1218 ? html`
1219 <sketch-tool-calls
1220 .toolCalls=${this.message?.tool_calls}
1221 .open=${this.open}
1222 ></sketch-tool-calls>
1223 `
1224 : ""}
1225
1226 <!-- Commits section (redesigned as bubbles) -->
1227 ${this.message?.commits
1228 ? html`
1229 <div class="commits-container">
1230 <div class="commit-notification">
1231 ${this.message.commits.length} new
1232 commit${this.message.commits.length > 1 ? "s" : ""}
1233 detected
1234 </div>
1235 ${this.message.commits.map((commit) => {
1236 return html`
1237 <div class="commit-card">
Philip Zeyliger72682df2025-04-23 13:09:46 -07001238 <span
1239 class="commit-hash"
1240 title="Click to copy: ${commit.hash}"
1241 @click=${(e) =>
1242 this.copyToClipboard(
1243 commit.hash.substring(0, 8),
1244 e,
1245 )}
1246 >
Pokey Rule7be879f2025-04-23 15:30:15 +01001247 ${commit.hash.substring(0, 8)}
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001248 </span>
1249 ${commit.pushed_branch
1250 ? html`
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001251 <span
1252 class="commit-branch pushed-branch"
1253 title="Click to copy: ${commit.pushed_branch}"
1254 @click=${(e) =>
1255 this.copyToClipboard(
1256 commit.pushed_branch,
1257 e,
1258 )}
1259 >${commit.pushed_branch}</span
1260 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001261 `
1262 : ``}
1263 <span class="commit-subject"
1264 >${commit.subject}</span
Sean McCullough71941bd2025-04-18 13:31:48 -07001265 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001266 <button
1267 class="commit-diff-button"
1268 @click=${() => this.showCommit(commit.hash)}
1269 >
1270 View Diff
1271 </button>
Sean McCullough86b56862025-04-18 13:04:03 -07001272 </div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001273 `;
1274 })}
1275 </div>
1276 `
1277 : ""}
1278 </div>
1279 </div>
1280
1281 <!-- Right side (empty for consistency) -->
1282 <div class="message-metadata-right"></div>
Sean McCullough86b56862025-04-18 13:04:03 -07001283 </div>
1284 </div>
1285 `;
1286 }
1287}
1288
Sean McCullough71941bd2025-04-18 13:31:48 -07001289function copyButton(textToCopy: string) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001290 // Use an icon of overlapping rectangles for copy
1291 const buttonClass = "copy-icon";
1292
1293 // SVG for copy icon (two overlapping rectangles)
1294 const copyIcon = html`<svg
1295 xmlns="http://www.w3.org/2000/svg"
1296 width="16"
1297 height="16"
1298 viewBox="0 0 24 24"
1299 fill="none"
1300 stroke="currentColor"
1301 stroke-width="2"
1302 stroke-linecap="round"
1303 stroke-linejoin="round"
1304 >
1305 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1306 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1307 </svg>`;
1308
1309 // SVG for success check mark
1310 const successIcon = html`<svg
1311 xmlns="http://www.w3.org/2000/svg"
1312 width="16"
1313 height="16"
1314 viewBox="0 0 24 24"
1315 fill="none"
1316 stroke="currentColor"
1317 stroke-width="2"
1318 stroke-linecap="round"
1319 stroke-linejoin="round"
1320 >
1321 <path d="M20 6L9 17l-5-5"></path>
1322 </svg>`;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001323
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001324 const ret = html`<button
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001325 class="${buttonClass}"
1326 title="Copy to clipboard"
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001327 @click=${(e: Event) => {
1328 e.stopPropagation();
1329 const copyButton = e.currentTarget as HTMLButtonElement;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001330 const originalInnerHTML = copyButton.innerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001331 navigator.clipboard
1332 .writeText(textToCopy)
1333 .then(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001334 copyButton.innerHTML = "";
1335 const successElement = document.createElement("div");
1336 copyButton.appendChild(successElement);
1337 render(successIcon, successElement);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001338 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001339 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001340 }, 2000);
1341 })
1342 .catch((err) => {
1343 console.error("Failed to copy text: ", err);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001344 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001345 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001346 }, 2000);
1347 });
1348 }}
1349 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001350 ${copyIcon}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001351 </button>`;
Sean McCullough86b56862025-04-18 13:04:03 -07001352
Sean McCullough71941bd2025-04-18 13:31:48 -07001353 return ret;
Sean McCullough86b56862025-04-18 13:04:03 -07001354}
1355
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001356// Create global styles for floating messages
1357const floatingMessageStyles = document.createElement("style");
1358floatingMessageStyles.textContent = `
1359 .floating-message {
1360 background-color: rgba(0, 0, 0, 0.8);
1361 color: white;
1362 padding: 5px 10px;
1363 border-radius: 4px;
1364 font-size: 12px;
1365 font-family: system-ui, sans-serif;
1366 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
1367 pointer-events: none;
1368 transition: opacity 0.3s ease, transform 0.3s ease;
1369 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001370
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001371 .floating-message.success {
1372 background-color: rgba(40, 167, 69, 0.9);
1373 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001374
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001375 .floating-message.error {
1376 background-color: rgba(220, 53, 69, 0.9);
1377 }
Philip Zeyligere31d2a92025-05-11 15:22:35 -07001378
1379 /* Style for code, pre elements, and tool components to ensure proper wrapping/truncation */
1380 pre, code, sketch-tool-calls, sketch-tool-card, sketch-tool-card-bash {
1381 white-space: nowrap;
1382 overflow: hidden;
1383 text-overflow: ellipsis;
1384 max-width: 100%;
1385 }
1386
1387 /* Special rule for the message content container */
1388 .message-content {
1389 max-width: 100% !important;
1390 overflow: hidden !important;
1391 }
1392
1393 /* Ensure tool call containers don't overflow */
1394 ::slotted(sketch-tool-calls) {
1395 max-width: 100%;
1396 width: 100%;
1397 overflow-wrap: break-word;
1398 word-break: break-word;
1399 }
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001400`;
1401document.head.appendChild(floatingMessageStyles);
1402
Sean McCullough86b56862025-04-18 13:04:03 -07001403declare global {
1404 interface HTMLElementTagNameMap {
1405 "sketch-timeline-message": SketchTimelineMessage;
1406 }
1407}