blob: 9ea1cedf05e779dce564330e0adf29f6b87f6dc4 [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";
philip.zeyliger6d3de482025-06-10 19:38:14 -07004import { AgentMessage, State } 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()
philip.zeyliger6d3de482025-06-10 19:38:14 -070015 state: State;
16
17 @property()
Sean McCulloughd9f13372025-04-21 15:08:49 -070018 previousMessage: AgentMessage;
Sean McCullough86b56862025-04-18 13:04:03 -070019
Sean McCullough2deac842025-04-21 18:17:57 -070020 @property()
21 open: boolean = false;
22
Philip Zeyligerb8a8f352025-06-02 07:39:37 -070023 @property()
24 firstMessageIndex: number = 0;
25
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000026 @state()
27 showInfo: boolean = false;
28
Sean McCullough86b56862025-04-18 13:04:03 -070029 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
30 // Note that these styles only apply to the scope of this web component's
31 // shadow DOM node, so they won't leak out or collide with CSS declared in
32 // other components or the containing web page (...unless you want it to do that).
33 static styles = css`
34 .message {
35 position: relative;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000036 margin-bottom: 6px;
37 display: flex;
38 flex-direction: column;
39 width: 100%;
Sean McCullough86b56862025-04-18 13:04:03 -070040 }
41
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000042 .message-container {
43 display: flex;
44 position: relative;
45 width: 100%;
46 }
47
48 .message-metadata-left {
49 flex: 0 0 80px;
50 padding: 3px 5px;
51 text-align: right;
52 font-size: 11px;
53 color: #777;
54 align-self: flex-start;
55 }
56
57 .message-metadata-right {
58 flex: 0 0 80px;
59 padding: 3px 5px;
60 text-align: left;
61 font-size: 11px;
62 color: #777;
63 align-self: flex-start;
64 }
65
66 .message-bubble-container {
67 flex: 1;
68 display: flex;
69 max-width: calc(100% - 160px);
Philip Zeyligere31d2a92025-05-11 15:22:35 -070070 overflow: hidden;
71 text-overflow: ellipsis;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000072 }
73
74 .user .message-bubble-container {
75 justify-content: flex-end;
76 }
77
78 .agent .message-bubble-container,
79 .tool .message-bubble-container,
80 .error .message-bubble-container {
81 justify-content: flex-start;
Sean McCullough86b56862025-04-18 13:04:03 -070082 }
83
84 .message-content {
85 position: relative;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000086 padding: 6px 10px;
87 border-radius: 12px;
88 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
Philip Zeyligere31d2a92025-05-11 15:22:35 -070089 max-width: 100%;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000090 width: fit-content;
91 min-width: min-content;
Philip Zeyligere31d2a92025-05-11 15:22:35 -070092 overflow-wrap: break-word;
93 word-break: break-word;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000094 }
95
96 /* User message styling */
97 .user .message-content {
98 background-color: #2196f3;
99 color: white;
100 border-bottom-right-radius: 5px;
101 }
102
103 /* Agent message styling */
104 .agent .message-content,
105 .tool .message-content,
106 .error .message-content {
107 background-color: #f1f1f1;
108 color: black;
109 border-bottom-left-radius: 5px;
Sean McCullough86b56862025-04-18 13:04:03 -0700110 }
111
112 /* Copy button styles */
113 .message-text-container,
114 .tool-result-container {
115 position: relative;
116 }
117
118 .message-actions {
119 position: absolute;
120 top: 5px;
121 right: 5px;
122 z-index: 10;
123 opacity: 0;
124 transition: opacity 0.2s ease;
125 }
126
127 .message-text-container:hover .message-actions,
128 .tool-result-container:hover .message-actions {
129 opacity: 1;
130 }
131
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000132 .message-actions {
Sean McCullough86b56862025-04-18 13:04:03 -0700133 display: flex;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000134 gap: 6px;
135 }
136
137 .copy-icon,
138 .info-icon {
139 background-color: transparent;
140 border: none;
141 color: rgba(0, 0, 0, 0.6);
142 cursor: pointer;
143 padding: 3px;
144 border-radius: 50%;
145 display: flex;
146 align-items: center;
147 justify-content: center;
148 width: 24px;
149 height: 24px;
150 transition: all 0.15s ease;
151 }
152
153 .user .copy-icon,
154 .user .info-icon {
155 color: rgba(255, 255, 255, 0.8);
156 }
157
158 .copy-icon:hover,
159 .info-icon:hover {
160 background-color: rgba(0, 0, 0, 0.08);
161 }
162
163 .user .copy-icon:hover,
164 .user .info-icon:hover {
165 background-color: rgba(255, 255, 255, 0.15);
166 }
167
168 /* Message metadata styling */
169 .message-type {
170 font-weight: bold;
171 font-size: 11px;
Sean McCullough86b56862025-04-18 13:04:03 -0700172 }
173
174 .message-timestamp {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000175 display: block;
Sean McCullough86b56862025-04-18 13:04:03 -0700176 font-size: 10px;
177 color: #888;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000178 margin-top: 2px;
179 }
180
181 .message-duration {
182 display: block;
183 font-size: 10px;
184 color: #888;
185 margin-top: 2px;
Sean McCullough86b56862025-04-18 13:04:03 -0700186 }
187
188 .message-usage {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000189 display: block;
Sean McCullough86b56862025-04-18 13:04:03 -0700190 font-size: 10px;
191 color: #888;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000192 margin-top: 3px;
Sean McCullough86b56862025-04-18 13:04:03 -0700193 }
194
195 .conversation-id {
196 font-family: monospace;
197 font-size: 12px;
198 padding: 2px 4px;
Sean McCullough86b56862025-04-18 13:04:03 -0700199 margin-left: auto;
200 }
201
202 .parent-info {
203 font-size: 11px;
204 opacity: 0.8;
205 }
206
207 .subconversation {
208 border-left: 2px solid transparent;
209 padding-left: 5px;
210 margin-left: 20px;
211 transition: margin-left 0.3s ease;
212 }
213
214 .message-text {
215 overflow-x: auto;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000216 margin-bottom: 0;
217 font-family: sans-serif;
218 padding: 2px 0;
Sean McCullough86b56862025-04-18 13:04:03 -0700219 user-select: text;
220 cursor: text;
221 -webkit-user-select: text;
222 -moz-user-select: text;
223 -ms-user-select: text;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000224 font-size: 14px;
225 line-height: 1.35;
226 text-align: left;
227 }
228
229 /* Style for code blocks within messages */
230 .message-text pre,
231 .message-text code {
232 font-family: monospace;
233 background: rgba(0, 0, 0, 0.05);
234 border-radius: 4px;
235 padding: 2px 4px;
236 overflow-x: auto;
237 max-width: 100%;
238 white-space: pre-wrap; /* Allow wrapping for very long lines */
239 word-break: break-all; /* Break words at any character */
240 box-sizing: border-box; /* Include padding in width calculation */
241 }
242
Pokey Rulea10f1512025-05-15 13:53:26 +0000243 /* Code block container styles */
244 .code-block-container {
245 position: relative;
246 margin: 8px 0;
247 border-radius: 6px;
248 overflow: hidden;
249 background: rgba(0, 0, 0, 0.05);
250 }
251
252 .user .code-block-container {
253 background: rgba(255, 255, 255, 0.2);
254 }
255
256 .code-block-header {
257 display: flex;
258 justify-content: space-between;
259 align-items: center;
260 padding: 4px 8px;
261 background: rgba(0, 0, 0, 0.1);
262 font-size: 12px;
263 }
264
265 .user .code-block-header {
266 background: rgba(255, 255, 255, 0.2);
267 color: white;
268 }
269
270 .code-language {
271 font-family: monospace;
272 font-size: 11px;
273 font-weight: 500;
274 }
275
276 .code-copy-button {
277 background: transparent;
278 border: none;
279 color: inherit;
280 cursor: pointer;
281 padding: 2px;
282 border-radius: 3px;
283 display: flex;
284 align-items: center;
285 justify-content: center;
286 opacity: 0.7;
287 transition: all 0.15s ease;
288 }
289
290 .code-copy-button:hover {
291 opacity: 1;
292 background: rgba(0, 0, 0, 0.1);
293 }
294
295 .user .code-copy-button:hover {
296 background: rgba(255, 255, 255, 0.2);
297 }
298
299 .code-block-container pre {
300 margin: 0;
301 padding: 8px;
302 background: transparent;
303 }
304
305 .code-block-container code {
306 background: transparent;
307 padding: 0;
308 display: block;
309 width: 100%;
310 }
311
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000312 .user .message-text pre,
313 .user .message-text code {
314 background: rgba(255, 255, 255, 0.2);
315 color: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700316 }
317
318 .tool-details {
319 margin-top: 3px;
320 padding-top: 3px;
321 border-top: 1px dashed #e0e0e0;
322 font-size: 12px;
323 }
324
325 .tool-name {
326 font-size: 12px;
327 font-weight: bold;
328 margin-bottom: 2px;
329 background: #f0f0f0;
330 padding: 2px 4px;
331 border-radius: 2px;
332 display: flex;
333 align-items: center;
334 gap: 3px;
335 }
336
337 .tool-input,
338 .tool-result {
339 margin-top: 2px;
340 padding: 3px 5px;
341 background: #f7f7f7;
342 border-radius: 2px;
343 font-family: monospace;
344 font-size: 12px;
345 overflow-x: auto;
346 white-space: pre;
347 line-height: 1.3;
348 user-select: text;
349 cursor: text;
350 -webkit-user-select: text;
351 -moz-user-select: text;
352 -ms-user-select: text;
353 }
354
355 .tool-result {
356 max-height: 300px;
357 overflow-y: auto;
358 }
359
360 .usage-info {
361 margin-top: 10px;
362 padding-top: 10px;
363 border-top: 1px dashed #e0e0e0;
364 font-size: 12px;
365 color: #666;
366 }
367
368 /* Custom styles for IRC-like experience */
369 .user .message-content {
370 border-left-color: #2196f3;
371 }
372
373 .agent .message-content {
374 border-left-color: #4caf50;
375 }
376
377 .tool .message-content {
378 border-left-color: #ff9800;
379 }
380
381 .error .message-content {
382 border-left-color: #f44336;
383 }
384
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700385 /* Compact message styling - distinct visual separation */
386 .compact {
387 background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
388 border: 2px solid #fd7e14;
389 border-radius: 12px;
390 margin: 20px 0;
391 padding: 0;
392 }
393
394 .compact .message-content {
395 border-left: 4px solid #fd7e14;
396 background: rgba(253, 126, 20, 0.05);
397 font-weight: 500;
398 }
399
400 .compact .message-text {
401 color: #8b4513;
402 font-size: 13px;
403 line-height: 1.4;
404 }
405
406 .compact::before {
407 content: "📚 CONVERSATION EPOCH";
408 display: block;
409 text-align: center;
410 font-size: 11px;
411 font-weight: bold;
412 color: #8b4513;
413 background: #fd7e14;
414 color: white;
415 padding: 4px 8px;
416 margin: 0;
417 border-radius: 8px 8px 0 0;
418 letter-spacing: 1px;
419 }
420
421 /* Pre-compaction messages get a subtle diagonal stripe background */
422 .pre-compaction {
423 background: repeating-linear-gradient(
424 45deg,
425 #ffffff,
426 #ffffff 10px,
427 #f8f8f8 10px,
428 #f8f8f8 20px
429 );
430 opacity: 0.85;
431 border-left: 3px solid #ddd;
432 }
433
434 .pre-compaction .message-content {
435 background: rgba(255, 255, 255, 0.7);
436 backdrop-filter: blur(1px);
Philip Zeyliger57d28bc2025-06-06 20:28:34 +0000437 color: #333; /* Ensure dark text for readability */
438 }
439
440 .pre-compaction .message-text {
441 color: #333; /* Ensure dark text in message content */
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700442 }
443
Sean McCullough86b56862025-04-18 13:04:03 -0700444 /* Make message type display bold but without the IRC-style markers */
445 .message-type {
446 font-weight: bold;
447 }
448
449 /* Commit message styling */
Sean McCullough86b56862025-04-18 13:04:03 -0700450 .commits-container {
451 margin-top: 10px;
Sean McCullough86b56862025-04-18 13:04:03 -0700452 }
453
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000454 .commit-notification {
455 background-color: #e8f5e9;
456 color: #2e7d32;
457 font-weight: 500;
458 font-size: 12px;
459 padding: 6px 10px;
460 border-radius: 10px;
461 margin-bottom: 8px;
462 text-align: center;
463 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
Sean McCullough86b56862025-04-18 13:04:03 -0700464 }
465
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000466 .commit-card {
467 background-color: #f5f5f5;
468 border-radius: 8px;
Sean McCullough86b56862025-04-18 13:04:03 -0700469 overflow: hidden;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000470 margin-bottom: 6px;
471 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
472 padding: 6px 8px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000473 display: flex;
474 align-items: center;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000475 gap: 8px;
Sean McCullough86b56862025-04-18 13:04:03 -0700476 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700477
Sean McCullough86b56862025-04-18 13:04:03 -0700478 .commit-hash {
479 color: #0366d6;
480 font-weight: bold;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000481 font-family: monospace;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000482 cursor: pointer;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000483 text-decoration: none;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000484 background-color: rgba(3, 102, 214, 0.08);
485 padding: 2px 5px;
486 border-radius: 4px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000487 }
488
489 .commit-hash:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000490 background-color: rgba(3, 102, 214, 0.15);
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000491 }
492
493 .commit-branch {
494 color: #28a745;
495 font-weight: 500;
496 cursor: pointer;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000497 font-family: monospace;
498 background-color: rgba(40, 167, 69, 0.08);
499 padding: 2px 5px;
500 border-radius: 4px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000501 }
502
503 .commit-branch:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000504 background-color: rgba(40, 167, 69, 0.15);
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000505 }
506
philip.zeyliger6d3de482025-06-10 19:38:14 -0700507 .commit-branch-container {
508 display: flex;
509 align-items: center;
510 gap: 6px;
511 }
512
513 .commit-branch-container .copy-icon {
514 opacity: 0.7;
515 display: flex;
516 align-items: center;
517 }
518
519 .commit-branch-container .copy-icon svg {
520 vertical-align: middle;
521 }
522
523 .commit-branch-container:hover .copy-icon {
524 opacity: 1;
525 }
526
527 .octocat-link {
528 color: #586069;
529 text-decoration: none;
530 display: flex;
531 align-items: center;
532 transition: color 0.2s ease;
533 }
534
535 .octocat-link:hover {
536 color: #0366d6;
537 }
538
539 .octocat-icon {
540 width: 14px;
541 height: 14px;
542 }
543
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000544 .commit-subject {
545 font-size: 13px;
546 color: #333;
547 flex-grow: 1;
548 overflow: hidden;
549 text-overflow: ellipsis;
550 white-space: nowrap;
Sean McCullough86b56862025-04-18 13:04:03 -0700551 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700552
Sean McCullough86b56862025-04-18 13:04:03 -0700553 .commit-diff-button {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000554 padding: 3px 8px;
555 border: none;
556 border-radius: 4px;
557 background-color: #0366d6;
558 color: white;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000559 font-size: 11px;
Sean McCullough86b56862025-04-18 13:04:03 -0700560 cursor: pointer;
561 transition: all 0.2s ease;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000562 display: block;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000563 margin-left: auto;
Sean McCullough86b56862025-04-18 13:04:03 -0700564 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700565
Sean McCullough86b56862025-04-18 13:04:03 -0700566 .commit-diff-button:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000567 background-color: #0256b4;
Sean McCullough86b56862025-04-18 13:04:03 -0700568 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700569
Sean McCullough86b56862025-04-18 13:04:03 -0700570 /* Tool call cards */
571 .tool-call-cards-container {
572 display: flex;
573 flex-direction: column;
574 gap: 8px;
575 margin-top: 8px;
576 }
577
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000578 /* Error message specific styling */
579 .error .message-content {
580 background-color: #ffebee;
581 border-left: 3px solid #f44336;
Sean McCullough86b56862025-04-18 13:04:03 -0700582 }
583
584 .end-of-turn {
585 margin-bottom: 15px;
586 }
587
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000588 .end-of-turn-indicator {
589 display: block;
590 font-size: 11px;
591 color: #777;
592 padding: 2px 0;
593 margin-top: 8px;
594 text-align: right;
595 font-style: italic;
596 }
597
598 .user .end-of-turn-indicator {
599 color: rgba(255, 255, 255, 0.7);
600 }
601
602 /* Message info panel styling */
603 .message-info-panel {
604 margin-top: 8px;
605 padding: 8px;
606 background-color: rgba(0, 0, 0, 0.03);
607 border-radius: 6px;
608 font-size: 12px;
609 transition: all 0.2s ease;
610 border-left: 2px solid rgba(0, 0, 0, 0.1);
611 }
612
613 .user .message-info-panel {
614 background-color: rgba(255, 255, 255, 0.15);
615 border-left: 2px solid rgba(255, 255, 255, 0.2);
616 }
617
618 .info-row {
619 margin-bottom: 3px;
620 display: flex;
621 }
622
623 .info-label {
624 font-weight: bold;
625 margin-right: 5px;
626 min-width: 60px;
627 }
628
629 .info-value {
630 flex: 1;
631 }
632
633 .conversation-id {
634 font-family: monospace;
Sean McCullough86b56862025-04-18 13:04:03 -0700635 font-size: 10px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000636 word-break: break-all;
Sean McCullough86b56862025-04-18 13:04:03 -0700637 }
638
639 .markdown-content {
640 box-sizing: border-box;
641 min-width: 200px;
642 margin: 0 auto;
643 }
644
645 .markdown-content p {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000646 margin-block-start: 0.3em;
647 margin-block-end: 0.3em;
648 }
649
650 .markdown-content p:first-child {
651 margin-block-start: 0;
652 }
653
654 .markdown-content p:last-child {
655 margin-block-end: 0;
656 }
657
658 /* Styling for markdown elements */
659 .markdown-content a {
660 color: inherit;
661 text-decoration: underline;
662 }
663
664 .user .markdown-content a {
665 color: #fff;
666 text-decoration: underline;
667 }
668
669 .markdown-content ul,
670 .markdown-content ol {
671 padding-left: 1.5em;
672 margin: 0.5em 0;
673 }
674
675 .markdown-content blockquote {
676 border-left: 3px solid rgba(0, 0, 0, 0.2);
677 padding-left: 1em;
678 margin-left: 0.5em;
679 font-style: italic;
680 }
681
682 .user .markdown-content blockquote {
683 border-left: 3px solid rgba(255, 255, 255, 0.4);
Sean McCullough86b56862025-04-18 13:04:03 -0700684 }
Autoformatterdded2d62025-04-28 00:27:21 +0000685
Sean McCullough8d93e362025-04-27 23:32:18 +0000686 /* Mermaid diagram styling */
687 .mermaid-container {
688 margin: 1em 0;
689 padding: 0.5em;
690 background-color: #f8f8f8;
691 border-radius: 4px;
692 overflow-x: auto;
693 }
Autoformatterdded2d62025-04-28 00:27:21 +0000694
Sean McCullough8d93e362025-04-27 23:32:18 +0000695 .mermaid {
696 text-align: center;
697 }
Sean McCullough86b56862025-04-18 13:04:03 -0700698 `;
699
Sean McCullough8d93e362025-04-27 23:32:18 +0000700 // Track mermaid diagrams that need rendering
701 private mermaidDiagrams = new Map();
702
Sean McCullough86b56862025-04-18 13:04:03 -0700703 constructor() {
704 super();
Sean McCullough8d93e362025-04-27 23:32:18 +0000705 // Initialize mermaid with specific config
706 mermaid.initialize({
707 startOnLoad: false,
Sean McCulloughf98d7302025-04-27 17:44:06 -0700708 suppressErrorRendering: true,
Autoformatterdded2d62025-04-28 00:27:21 +0000709 theme: "default",
710 securityLevel: "loose", // Allows more flexibility but be careful with user-generated content
711 fontFamily: "monospace",
Sean McCullough8d93e362025-04-27 23:32:18 +0000712 });
Sean McCullough86b56862025-04-18 13:04:03 -0700713 }
714
715 // See https://lit.dev/docs/components/lifecycle/
716 connectedCallback() {
717 super.connectedCallback();
718 }
Autoformatterdded2d62025-04-28 00:27:21 +0000719
Sean McCullough8d93e362025-04-27 23:32:18 +0000720 // After the component is updated and rendered, render any mermaid diagrams
721 updated(changedProperties: Map<string, unknown>) {
722 super.updated(changedProperties);
723 this.renderMermaidDiagrams();
Pokey Rulea10f1512025-05-15 13:53:26 +0000724 this.setupCodeBlockCopyButtons();
Sean McCullough8d93e362025-04-27 23:32:18 +0000725 }
Autoformatterdded2d62025-04-28 00:27:21 +0000726
Sean McCullough8d93e362025-04-27 23:32:18 +0000727 // Render mermaid diagrams after the component is updated
728 renderMermaidDiagrams() {
729 // Add a small delay to ensure the DOM is fully rendered
730 setTimeout(() => {
731 // Find all mermaid containers in our shadow root
Autoformatterdded2d62025-04-28 00:27:21 +0000732 const containers = this.shadowRoot?.querySelectorAll(".mermaid");
Sean McCullough8d93e362025-04-27 23:32:18 +0000733 if (!containers || containers.length === 0) return;
Autoformatterdded2d62025-04-28 00:27:21 +0000734
Sean McCullough8d93e362025-04-27 23:32:18 +0000735 // Process each mermaid diagram
Autoformatterdded2d62025-04-28 00:27:21 +0000736 containers.forEach((container) => {
Sean McCullough8d93e362025-04-27 23:32:18 +0000737 const id = container.id;
Autoformatterdded2d62025-04-28 00:27:21 +0000738 const code = container.textContent || "";
Sean McCullough8d93e362025-04-27 23:32:18 +0000739 if (!code || !id) return; // Use return for forEach instead of continue
Autoformatterdded2d62025-04-28 00:27:21 +0000740
Sean McCullough8d93e362025-04-27 23:32:18 +0000741 try {
742 // Clear any previous content
743 container.innerHTML = code;
Autoformatterdded2d62025-04-28 00:27:21 +0000744
Sean McCullough8d93e362025-04-27 23:32:18 +0000745 // Render the mermaid diagram using promise
Autoformatterdded2d62025-04-28 00:27:21 +0000746 mermaid
747 .render(`${id}-svg`, code)
Sean McCullough8d93e362025-04-27 23:32:18 +0000748 .then(({ svg }) => {
749 container.innerHTML = svg;
750 })
Autoformatterdded2d62025-04-28 00:27:21 +0000751 .catch((err) => {
752 console.error("Error rendering mermaid diagram:", err);
Sean McCullough8d93e362025-04-27 23:32:18 +0000753 // Show the original code as fallback
754 container.innerHTML = `<pre>${code}</pre>`;
755 });
756 } catch (err) {
Autoformatterdded2d62025-04-28 00:27:21 +0000757 console.error("Error processing mermaid diagram:", err);
Sean McCullough8d93e362025-04-27 23:32:18 +0000758 // Show the original code as fallback
759 container.innerHTML = `<pre>${code}</pre>`;
760 }
761 });
762 }, 100); // Small delay to ensure DOM is ready
763 }
Sean McCullough86b56862025-04-18 13:04:03 -0700764
Pokey Rulea10f1512025-05-15 13:53:26 +0000765 // Setup code block copy buttons after component is updated
766 setupCodeBlockCopyButtons() {
767 setTimeout(() => {
768 // Find all copy buttons in code blocks
769 const copyButtons =
770 this.shadowRoot?.querySelectorAll(".code-copy-button");
771 if (!copyButtons || copyButtons.length === 0) return;
772
773 // Add click event listener to each button
774 copyButtons.forEach((button) => {
775 button.addEventListener("click", (e) => {
776 e.stopPropagation();
777 const codeId = (button as HTMLElement).dataset.codeId;
778 if (!codeId) return;
779
780 const codeElement = this.shadowRoot?.querySelector(`#${codeId}`);
781 if (!codeElement) return;
782
783 const codeText = codeElement.textContent || "";
784 const buttonRect = button.getBoundingClientRect();
785
786 // Copy code to clipboard
787 navigator.clipboard
788 .writeText(codeText)
789 .then(() => {
790 // Show success indicator
791 const originalHTML = button.innerHTML;
792 button.innerHTML = `
793 <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">
794 <path d="M20 6L9 17l-5-5"></path>
795 </svg>
796 `;
797
798 // Display floating message
799 this.showFloatingMessage("Copied!", buttonRect, "success");
800
801 // Reset button after delay
802 setTimeout(() => {
803 button.innerHTML = originalHTML;
804 }, 2000);
805 })
806 .catch((err) => {
807 console.error("Failed to copy code:", err);
808 this.showFloatingMessage("Failed to copy!", buttonRect, "error");
809 });
810 });
811 });
812 }, 100); // Small delay to ensure DOM is ready
813 }
814
Sean McCullough86b56862025-04-18 13:04:03 -0700815 // See https://lit.dev/docs/components/lifecycle/
816 disconnectedCallback() {
817 super.disconnectedCallback();
818 }
819
820 renderMarkdown(markdownContent: string): string {
821 try {
Sean McCullough8d93e362025-04-27 23:32:18 +0000822 // Create a custom renderer
823 const renderer = new Renderer();
824 const originalCodeRenderer = renderer.code.bind(renderer);
Autoformatterdded2d62025-04-28 00:27:21 +0000825
Pokey Rulea10f1512025-05-15 13:53:26 +0000826 // Override the code renderer to handle mermaid diagrams and add copy buttons
Autoformatterdded2d62025-04-28 00:27:21 +0000827 renderer.code = function ({ text, lang, escaped }: Tokens.Code): string {
828 if (lang === "mermaid") {
Sean McCullough8d93e362025-04-27 23:32:18 +0000829 // Generate a unique ID for this diagram
830 const id = `mermaid-diagram-${Math.random().toString(36).substring(2, 10)}`;
Autoformatterdded2d62025-04-28 00:27:21 +0000831
Sean McCullough8d93e362025-04-27 23:32:18 +0000832 // Just create the container and mermaid div - we'll render it in the updated() lifecycle method
833 return `<div class="mermaid-container">
834 <div class="mermaid" id="${id}">${text}</div>
835 </div>`;
836 }
Pokey Rulea10f1512025-05-15 13:53:26 +0000837
Philip Zeyliger0d092842025-06-09 18:57:12 -0700838 // For regular code blocks, call the original renderer to get properly escaped HTML
839 const originalCodeHtml = originalCodeRenderer({ text, lang, escaped });
840
841 // Extract the code content from the original HTML to add our custom wrapper
842 // The original renderer returns: <pre><code class="language-x">escapedText</code></pre>
843 const codeMatch = originalCodeHtml.match(
844 /<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/,
845 );
846 if (!codeMatch) {
847 // Fallback to original if we can't parse it
848 return originalCodeHtml;
849 }
850
851 const escapedText = codeMatch[1];
Pokey Rulea10f1512025-05-15 13:53:26 +0000852 const id = `code-block-${Math.random().toString(36).substring(2, 10)}`;
853 const langClass = lang ? ` class="language-${lang}"` : "";
854
855 return `<div class="code-block-container">
856 <div class="code-block-header">
857 ${lang ? `<span class="code-language">${lang}</span>` : ""}
858 <button class="code-copy-button" title="Copy code" data-code-id="${id}">
859 <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">
860 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
861 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
862 </svg>
863 </button>
864 </div>
Philip Zeyliger0d092842025-06-09 18:57:12 -0700865 <pre><code id="${id}"${langClass}>${escapedText}</code></pre>
Pokey Rulea10f1512025-05-15 13:53:26 +0000866 </div>`;
Sean McCullough8d93e362025-04-27 23:32:18 +0000867 };
Autoformatterdded2d62025-04-28 00:27:21 +0000868
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000869 // Set markdown options for proper code block highlighting
Sean McCullough86b56862025-04-18 13:04:03 -0700870 const markedOptions: MarkedOptions = {
871 gfm: true, // GitHub Flavored Markdown
872 breaks: true, // Convert newlines to <br>
873 async: false,
Autoformatterdded2d62025-04-28 00:27:21 +0000874 renderer: renderer,
Sean McCullough86b56862025-04-18 13:04:03 -0700875 };
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000876
877 // Parse markdown and sanitize the output HTML with DOMPurify
878 const htmlOutput = marked.parse(markdownContent, markedOptions) as string;
879 return DOMPurify.sanitize(htmlOutput, {
880 // Allow common HTML elements that are safe
881 ALLOWED_TAGS: [
882 "p",
883 "br",
884 "strong",
885 "em",
886 "b",
887 "i",
888 "u",
889 "s",
890 "code",
891 "pre",
892 "h1",
893 "h2",
894 "h3",
895 "h4",
896 "h5",
897 "h6",
898 "ul",
899 "ol",
900 "li",
901 "blockquote",
902 "a",
903 "div",
904 "span", // For mermaid diagrams and code blocks
905 "svg",
906 "g",
907 "path",
908 "rect",
909 "circle",
910 "text",
911 "line",
912 "polygon", // For mermaid SVG
913 "button", // For code copy buttons
914 ],
915 ALLOWED_ATTR: [
916 "href",
917 "title",
918 "target",
919 "rel", // For links
920 "class",
921 "id", // For styling and functionality
922 "data-*", // For code copy buttons
923 // SVG attributes for mermaid diagrams
924 "viewBox",
925 "width",
926 "height",
927 "xmlns",
928 "fill",
929 "stroke",
930 "stroke-width",
931 "d",
932 "x",
933 "y",
934 "x1",
935 "y1",
936 "x2",
937 "y2",
938 "cx",
939 "cy",
940 "r",
941 "rx",
942 "ry",
943 "points",
944 "transform",
945 "text-anchor",
946 "font-size",
947 "font-family",
948 ],
949 // Allow data attributes for functionality
950 ALLOW_DATA_ATTR: true,
951 // Keep whitespace for code formatting
952 KEEP_CONTENT: true,
953 });
Sean McCullough86b56862025-04-18 13:04:03 -0700954 } catch (error) {
955 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000956 // Fallback to sanitized plain text if markdown parsing fails
957 return DOMPurify.sanitize(markdownContent);
Sean McCullough86b56862025-04-18 13:04:03 -0700958 }
959 }
960
961 /**
962 * Format timestamp for display
963 */
964 formatTimestamp(
965 timestamp: string | number | Date | null | undefined,
966 defaultValue: string = "",
967 ): string {
968 if (!timestamp) return defaultValue;
969 try {
970 const date = new Date(timestamp);
971 if (isNaN(date.getTime())) return defaultValue;
972
973 // Format: Mar 13, 2025 09:53:25 AM
974 return date.toLocaleString("en-US", {
975 month: "short",
976 day: "numeric",
977 year: "numeric",
978 hour: "numeric",
979 minute: "2-digit",
980 second: "2-digit",
981 hour12: true,
982 });
983 } catch (e) {
984 return defaultValue;
985 }
986 }
987
988 formatNumber(
989 num: number | null | undefined,
990 defaultValue: string = "0",
991 ): string {
992 if (num === undefined || num === null) return defaultValue;
993 try {
994 return num.toLocaleString();
995 } catch (e) {
996 return String(num);
997 }
998 }
999 formatCurrency(
1000 num: number | string | null | undefined,
1001 defaultValue: string = "$0.00",
1002 isMessageLevel: boolean = false,
1003 ): string {
1004 if (num === undefined || num === null) return defaultValue;
1005 try {
1006 // Use 4 decimal places for message-level costs, 2 for totals
1007 const decimalPlaces = isMessageLevel ? 4 : 2;
1008 return `$${parseFloat(String(num)).toFixed(decimalPlaces)}`;
1009 } catch (e) {
1010 return defaultValue;
1011 }
1012 }
1013
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001014 // Format duration from nanoseconds to a human-readable string
1015 _formatDuration(nanoseconds: number | null | undefined): string {
1016 if (!nanoseconds) return "0s";
1017
1018 const seconds = nanoseconds / 1e9;
1019
1020 if (seconds < 60) {
1021 return `${seconds.toFixed(1)}s`;
1022 } else if (seconds < 3600) {
1023 const minutes = Math.floor(seconds / 60);
1024 const remainingSeconds = seconds % 60;
1025 return `${minutes}min ${remainingSeconds.toFixed(0)}s`;
1026 } else {
1027 const hours = Math.floor(seconds / 3600);
1028 const remainingSeconds = seconds % 3600;
1029 const minutes = Math.floor(remainingSeconds / 60);
1030 return `${hours}h ${minutes}min`;
1031 }
1032 }
1033
Sean McCullough86b56862025-04-18 13:04:03 -07001034 showCommit(commitHash: string) {
Sean McCullough71941bd2025-04-18 13:31:48 -07001035 this.dispatchEvent(
1036 new CustomEvent("show-commit-diff", {
1037 bubbles: true,
1038 composed: true,
1039 detail: { commitHash },
1040 }),
1041 );
Sean McCullough86b56862025-04-18 13:04:03 -07001042 }
1043
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001044 _toggleInfo(e: Event) {
1045 e.stopPropagation();
1046 this.showInfo = !this.showInfo;
1047 }
1048
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001049 copyToClipboard(text: string, event: Event) {
1050 const element = event.currentTarget as HTMLElement;
1051 const rect = element.getBoundingClientRect();
1052
1053 navigator.clipboard
1054 .writeText(text)
1055 .then(() => {
1056 this.showFloatingMessage("Copied!", rect, "success");
1057 })
1058 .catch((err) => {
1059 console.error("Failed to copy text: ", err);
1060 this.showFloatingMessage("Failed to copy!", rect, "error");
1061 });
1062 }
1063
1064 showFloatingMessage(
1065 message: string,
1066 targetRect: DOMRect,
1067 type: "success" | "error",
1068 ) {
1069 // Create floating message element
1070 const floatingMsg = document.createElement("div");
1071 floatingMsg.textContent = message;
1072 floatingMsg.className = `floating-message ${type}`;
1073
1074 // Position it near the clicked element
1075 // Position just above the element
1076 const top = targetRect.top - 30;
1077 const left = targetRect.left + targetRect.width / 2 - 40;
1078
1079 floatingMsg.style.position = "fixed";
1080 floatingMsg.style.top = `${top}px`;
1081 floatingMsg.style.left = `${left}px`;
1082 floatingMsg.style.zIndex = "9999";
1083
1084 // Add to document body
1085 document.body.appendChild(floatingMsg);
1086
1087 // Animate in
1088 floatingMsg.style.opacity = "0";
1089 floatingMsg.style.transform = "translateY(10px)";
1090
1091 setTimeout(() => {
1092 floatingMsg.style.opacity = "1";
1093 floatingMsg.style.transform = "translateY(0)";
1094 }, 10);
1095
1096 // Remove after animation
1097 setTimeout(() => {
1098 floatingMsg.style.opacity = "0";
1099 floatingMsg.style.transform = "translateY(-10px)";
1100
1101 setTimeout(() => {
1102 document.body.removeChild(floatingMsg);
1103 }, 300);
1104 }, 1500);
1105 }
1106
philip.zeyliger6d3de482025-06-10 19:38:14 -07001107 // Format GitHub repository URL to org/repo format
1108 formatGitHubRepo(url) {
1109 if (!url) return null;
1110
1111 // Common GitHub URL patterns
1112 const patterns = [
1113 // HTTPS URLs
1114 /https:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
1115 // SSH URLs
1116 /git@github\.com:([^/]+)\/([^/\s.]+)(?:\.git)?/,
1117 // Git protocol
1118 /git:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
1119 ];
1120
1121 for (const pattern of patterns) {
1122 const match = url.match(pattern);
1123 if (match) {
1124 return {
1125 formatted: `${match[1]}/${match[2]}`,
1126 url: `https://github.com/${match[1]}/${match[2]}`,
1127 owner: match[1],
1128 repo: match[2],
1129 };
1130 }
1131 }
1132
1133 return null;
1134 }
1135
1136 // Generate GitHub branch URL if linking is enabled
1137 getGitHubBranchLink(branchName) {
1138 if (!this.state?.link_to_github || !branchName) {
1139 return null;
1140 }
1141
1142 const github = this.formatGitHubRepo(this.state?.git_origin);
1143 if (!github) {
1144 return null;
1145 }
1146
1147 return `https://github.com/${github.owner}/${github.repo}/tree/${branchName}`;
1148 }
1149
Sean McCullough86b56862025-04-18 13:04:03 -07001150 render() {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001151 // Calculate if this is an end of turn message with no parent conversation ID
1152 const isEndOfTurn =
1153 this.message?.end_of_turn && !this.message?.parent_conversation_id;
1154
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001155 const isPreCompaction =
1156 this.message?.idx !== undefined &&
1157 this.message.idx < this.firstMessageIndex;
1158
Sean McCullough86b56862025-04-18 13:04:03 -07001159 return html`
1160 <div
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001161 class="message ${this.message?.type} ${isEndOfTurn
Sean McCullough86b56862025-04-18 13:04:03 -07001162 ? "end-of-turn"
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001163 : ""} ${isPreCompaction ? "pre-compaction" : ""}"
Sean McCullough86b56862025-04-18 13:04:03 -07001164 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001165 <div class="message-container">
1166 <!-- Left area (empty for simplicity) -->
1167 <div class="message-metadata-left"></div>
1168
1169 <!-- Message bubble -->
1170 <div class="message-bubble-container">
1171 <div class="message-content">
1172 <div class="message-text-container">
1173 <div class="message-actions">
1174 ${copyButton(this.message?.content)}
1175 <button
1176 class="info-icon"
1177 title="Show message details"
1178 @click=${this._toggleInfo}
Sean McCullough71941bd2025-04-18 13:31:48 -07001179 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001180 <svg
1181 xmlns="http://www.w3.org/2000/svg"
1182 width="16"
1183 height="16"
1184 viewBox="0 0 24 24"
1185 fill="none"
1186 stroke="currentColor"
1187 stroke-width="2"
1188 stroke-linecap="round"
1189 stroke-linejoin="round"
1190 >
1191 <circle cx="12" cy="12" r="10"></circle>
1192 <line x1="12" y1="16" x2="12" y2="12"></line>
1193 <line x1="12" y1="8" x2="12.01" y2="8"></line>
1194 </svg>
1195 </button>
1196 </div>
1197 ${this.message?.content
1198 ? html`
1199 <div class="message-text markdown-content">
1200 ${unsafeHTML(
1201 this.renderMarkdown(this.message?.content),
1202 )}
1203 </div>
1204 `
1205 : ""}
1206
1207 <!-- End of turn indicator inside the bubble -->
1208 ${isEndOfTurn && this.message?.elapsed
1209 ? html`
1210 <div class="end-of-turn-indicator">
1211 end of turn
1212 (${this._formatDuration(this.message?.elapsed)})
1213 </div>
1214 `
1215 : ""}
1216
1217 <!-- Info panel that can be toggled -->
1218 ${this.showInfo
1219 ? html`
1220 <div class="message-info-panel">
1221 <div class="info-row">
1222 <span class="info-label">Type:</span>
1223 <span class="info-value">${this.message?.type}</span>
1224 </div>
1225 <div class="info-row">
1226 <span class="info-label">Time:</span>
1227 <span class="info-value"
1228 >${this.formatTimestamp(
1229 this.message?.timestamp,
1230 "",
1231 )}</span
1232 >
1233 </div>
1234 ${this.message?.elapsed
1235 ? html`
1236 <div class="info-row">
1237 <span class="info-label">Duration:</span>
1238 <span class="info-value"
1239 >${this._formatDuration(
1240 this.message?.elapsed,
1241 )}</span
1242 >
1243 </div>
1244 `
1245 : ""}
1246 ${this.message?.usage
1247 ? html`
1248 <div class="info-row">
1249 <span class="info-label">Tokens:</span>
1250 <span class="info-value">
1251 ${this.message?.usage
1252 ? html`
1253 <div>
1254 Input:
1255 ${this.formatNumber(
1256 this.message?.usage?.input_tokens ||
1257 0,
1258 )}
1259 </div>
1260 ${this.message?.usage
1261 ?.cache_creation_input_tokens
1262 ? html`
1263 <div>
1264 Cache creation:
1265 ${this.formatNumber(
1266 this.message?.usage
1267 ?.cache_creation_input_tokens,
1268 )}
1269 </div>
1270 `
1271 : ""}
1272 ${this.message?.usage
1273 ?.cache_read_input_tokens
1274 ? html`
1275 <div>
1276 Cache read:
1277 ${this.formatNumber(
1278 this.message?.usage
1279 ?.cache_read_input_tokens,
1280 )}
1281 </div>
1282 `
1283 : ""}
1284 <div>
1285 Output:
1286 ${this.formatNumber(
1287 this.message?.usage?.output_tokens,
1288 )}
1289 </div>
1290 <div>
1291 Cost:
1292 ${this.formatCurrency(
1293 this.message?.usage?.cost_usd,
1294 )}
1295 </div>
1296 `
1297 : "N/A"}
1298 </span>
1299 </div>
1300 `
1301 : ""}
1302 ${this.message?.conversation_id
1303 ? html`
1304 <div class="info-row">
1305 <span class="info-label">Conversation ID:</span>
1306 <span class="info-value conversation-id"
1307 >${this.message?.conversation_id}</span
1308 >
1309 </div>
1310 `
1311 : ""}
1312 </div>
1313 `
1314 : ""}
1315 </div>
1316
1317 <!-- Tool calls - only shown for agent messages -->
1318 ${this.message?.type === "agent"
1319 ? html`
1320 <sketch-tool-calls
1321 .toolCalls=${this.message?.tool_calls}
1322 .open=${this.open}
1323 ></sketch-tool-calls>
1324 `
1325 : ""}
1326
1327 <!-- Commits section (redesigned as bubbles) -->
1328 ${this.message?.commits
1329 ? html`
1330 <div class="commits-container">
1331 <div class="commit-notification">
1332 ${this.message.commits.length} new
1333 commit${this.message.commits.length > 1 ? "s" : ""}
1334 detected
1335 </div>
1336 ${this.message.commits.map((commit) => {
1337 return html`
1338 <div class="commit-card">
Philip Zeyliger72682df2025-04-23 13:09:46 -07001339 <span
1340 class="commit-hash"
1341 title="Click to copy: ${commit.hash}"
1342 @click=${(e) =>
1343 this.copyToClipboard(
1344 commit.hash.substring(0, 8),
1345 e,
1346 )}
1347 >
Pokey Rule7be879f2025-04-23 15:30:15 +01001348 ${commit.hash.substring(0, 8)}
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001349 </span>
1350 ${commit.pushed_branch
philip.zeyliger6d3de482025-06-10 19:38:14 -07001351 ? (() => {
1352 const githubLink = this.getGitHubBranchLink(
1353 commit.pushed_branch,
1354 );
1355 return html`
1356 <div class="commit-branch-container">
1357 <span
1358 class="commit-branch pushed-branch"
1359 title="Click to copy: ${commit.pushed_branch}"
1360 @click=${(e) =>
1361 this.copyToClipboard(
1362 commit.pushed_branch,
1363 e,
1364 )}
1365 >${commit.pushed_branch}</span
1366 >
1367 <span class="copy-icon">
1368 <svg
1369 xmlns="http://www.w3.org/2000/svg"
1370 width="14"
1371 height="14"
1372 viewBox="0 0 24 24"
1373 fill="none"
1374 stroke="currentColor"
1375 stroke-width="2"
1376 stroke-linecap="round"
1377 stroke-linejoin="round"
1378 >
1379 <rect
1380 x="9"
1381 y="9"
1382 width="13"
1383 height="13"
1384 rx="2"
1385 ry="2"
1386 ></rect>
1387 <path
1388 d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
1389 ></path>
1390 </svg>
1391 </span>
1392 ${githubLink
1393 ? html`
1394 <a
1395 href="${githubLink}"
1396 target="_blank"
1397 rel="noopener noreferrer"
1398 class="octocat-link"
1399 title="Open ${commit.pushed_branch} on GitHub"
1400 @click=${(e) =>
1401 e.stopPropagation()}
1402 >
1403 <svg
1404 class="octocat-icon"
1405 viewBox="0 0 16 16"
1406 width="14"
1407 height="14"
1408 >
1409 <path
1410 fill="currentColor"
1411 d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
1412 />
1413 </svg>
1414 </a>
1415 `
1416 : ""}
1417 </div>
1418 `;
1419 })()
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001420 : ``}
1421 <span class="commit-subject"
1422 >${commit.subject}</span
Sean McCullough71941bd2025-04-18 13:31:48 -07001423 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001424 <button
1425 class="commit-diff-button"
1426 @click=${() => this.showCommit(commit.hash)}
1427 >
1428 View Diff
1429 </button>
Sean McCullough86b56862025-04-18 13:04:03 -07001430 </div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001431 `;
1432 })}
1433 </div>
1434 `
1435 : ""}
1436 </div>
1437 </div>
1438
1439 <!-- Right side (empty for consistency) -->
1440 <div class="message-metadata-right"></div>
Sean McCullough86b56862025-04-18 13:04:03 -07001441 </div>
1442 </div>
1443 `;
1444 }
1445}
1446
Sean McCullough71941bd2025-04-18 13:31:48 -07001447function copyButton(textToCopy: string) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001448 // Use an icon of overlapping rectangles for copy
1449 const buttonClass = "copy-icon";
1450
1451 // SVG for copy icon (two overlapping rectangles)
1452 const copyIcon = html`<svg
1453 xmlns="http://www.w3.org/2000/svg"
1454 width="16"
1455 height="16"
1456 viewBox="0 0 24 24"
1457 fill="none"
1458 stroke="currentColor"
1459 stroke-width="2"
1460 stroke-linecap="round"
1461 stroke-linejoin="round"
1462 >
1463 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1464 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1465 </svg>`;
1466
1467 // SVG for success check mark
1468 const successIcon = html`<svg
1469 xmlns="http://www.w3.org/2000/svg"
1470 width="16"
1471 height="16"
1472 viewBox="0 0 24 24"
1473 fill="none"
1474 stroke="currentColor"
1475 stroke-width="2"
1476 stroke-linecap="round"
1477 stroke-linejoin="round"
1478 >
1479 <path d="M20 6L9 17l-5-5"></path>
1480 </svg>`;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001481
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001482 const ret = html`<button
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001483 class="${buttonClass}"
1484 title="Copy to clipboard"
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001485 @click=${(e: Event) => {
1486 e.stopPropagation();
1487 const copyButton = e.currentTarget as HTMLButtonElement;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001488 const originalInnerHTML = copyButton.innerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001489 navigator.clipboard
1490 .writeText(textToCopy)
1491 .then(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001492 copyButton.innerHTML = "";
1493 const successElement = document.createElement("div");
1494 copyButton.appendChild(successElement);
1495 render(successIcon, successElement);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001496 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001497 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001498 }, 2000);
1499 })
1500 .catch((err) => {
1501 console.error("Failed to copy text: ", err);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001502 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001503 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001504 }, 2000);
1505 });
1506 }}
1507 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001508 ${copyIcon}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001509 </button>`;
Sean McCullough86b56862025-04-18 13:04:03 -07001510
Sean McCullough71941bd2025-04-18 13:31:48 -07001511 return ret;
Sean McCullough86b56862025-04-18 13:04:03 -07001512}
1513
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001514// Create global styles for floating messages
1515const floatingMessageStyles = document.createElement("style");
1516floatingMessageStyles.textContent = `
1517 .floating-message {
1518 background-color: rgba(0, 0, 0, 0.8);
1519 color: white;
1520 padding: 5px 10px;
1521 border-radius: 4px;
1522 font-size: 12px;
1523 font-family: system-ui, sans-serif;
1524 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
1525 pointer-events: none;
1526 transition: opacity 0.3s ease, transform 0.3s ease;
1527 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001528
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001529 .floating-message.success {
1530 background-color: rgba(40, 167, 69, 0.9);
1531 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001532
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001533 .floating-message.error {
1534 background-color: rgba(220, 53, 69, 0.9);
1535 }
Philip Zeyligere31d2a92025-05-11 15:22:35 -07001536
1537 /* Style for code, pre elements, and tool components to ensure proper wrapping/truncation */
1538 pre, code, sketch-tool-calls, sketch-tool-card, sketch-tool-card-bash {
1539 white-space: nowrap;
1540 overflow: hidden;
1541 text-overflow: ellipsis;
1542 max-width: 100%;
1543 }
1544
1545 /* Special rule for the message content container */
1546 .message-content {
1547 max-width: 100% !important;
1548 overflow: hidden !important;
1549 }
1550
1551 /* Ensure tool call containers don't overflow */
1552 ::slotted(sketch-tool-calls) {
1553 max-width: 100%;
1554 width: 100%;
1555 overflow-wrap: break-word;
1556 word-break: break-word;
1557 }
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001558`;
1559document.head.appendChild(floatingMessageStyles);
1560
Sean McCullough86b56862025-04-18 13:04:03 -07001561declare global {
1562 interface HTMLElementTagNameMap {
1563 "sketch-timeline-message": SketchTimelineMessage;
1564 }
1565}