blob: 9fa5b7ae41232fd4d97d3600bc0ff62616ff0421 [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";
philip.zeyliger7c1a6872025-06-16 03:54:37 +00006import type mermaid from "mermaid";
Philip Zeyliger53ab2452025-06-04 17:49:33 +00007import DOMPurify from "dompurify";
philip.zeyliger7c1a6872025-06-16 03:54:37 +00008
9// Mermaid is loaded dynamically - see loadMermaid() function
10declare global {
11 interface Window {
12 mermaid?: typeof mermaid;
13 }
14}
15
16// Mermaid hash will be injected at build time
17declare const __MERMAID_HASH__: string;
18
19// Load Mermaid dynamically
20let mermaidLoadPromise: Promise<any> | null = null;
21
22function loadMermaid(): Promise<typeof mermaid> {
23 if (mermaidLoadPromise) {
24 return mermaidLoadPromise;
25 }
26
27 if (window.mermaid) {
28 return Promise.resolve(window.mermaid);
29 }
30
31 mermaidLoadPromise = new Promise((resolve, reject) => {
32 // Get the Mermaid hash from build-time constant
33 const mermaidHash = __MERMAID_HASH__;
34
35 // Try to load the external Mermaid bundle
36 const script = document.createElement("script");
37 script.onload = () => {
38 // The Mermaid bundle should set window.mermaid
39 if (window.mermaid) {
40 resolve(window.mermaid);
41 } else {
42 reject(new Error("Mermaid not loaded from external bundle"));
43 }
44 };
45 script.onerror = (error) => {
46 console.warn("Failed to load external Mermaid bundle:", error);
47 reject(new Error("Mermaid external bundle failed to load"));
48 };
49
50 // Don't set type="module" since we're using IIFE format
51 script.src = `./static/mermaid-standalone-${mermaidHash}.js`;
52 document.head.appendChild(script);
53 });
54
55 return mermaidLoadPromise;
56}
Sean McCullough86b56862025-04-18 13:04:03 -070057import "./sketch-tool-calls";
58@customElement("sketch-timeline-message")
59export class SketchTimelineMessage extends LitElement {
60 @property()
Sean McCulloughd9f13372025-04-21 15:08:49 -070061 message: AgentMessage;
Sean McCullough86b56862025-04-18 13:04:03 -070062
63 @property()
philip.zeyliger6d3de482025-06-10 19:38:14 -070064 state: State;
65
66 @property()
Sean McCulloughd9f13372025-04-21 15:08:49 -070067 previousMessage: AgentMessage;
Sean McCullough86b56862025-04-18 13:04:03 -070068
Sean McCullough2deac842025-04-21 18:17:57 -070069 @property()
70 open: boolean = false;
71
Philip Zeyligerb8a8f352025-06-02 07:39:37 -070072 @property()
73 firstMessageIndex: number = 0;
74
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000075 @state()
76 showInfo: boolean = false;
77
Sean McCullough86b56862025-04-18 13:04:03 -070078 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
79 // Note that these styles only apply to the scope of this web component's
80 // shadow DOM node, so they won't leak out or collide with CSS declared in
81 // other components or the containing web page (...unless you want it to do that).
82 static styles = css`
83 .message {
84 position: relative;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000085 margin-bottom: 6px;
86 display: flex;
87 flex-direction: column;
88 width: 100%;
Sean McCullough86b56862025-04-18 13:04:03 -070089 }
90
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000091 .message-container {
92 display: flex;
93 position: relative;
94 width: 100%;
95 }
96
97 .message-metadata-left {
98 flex: 0 0 80px;
99 padding: 3px 5px;
100 text-align: right;
101 font-size: 11px;
102 color: #777;
103 align-self: flex-start;
104 }
105
106 .message-metadata-right {
107 flex: 0 0 80px;
108 padding: 3px 5px;
109 text-align: left;
110 font-size: 11px;
111 color: #777;
112 align-self: flex-start;
113 }
114
115 .message-bubble-container {
116 flex: 1;
117 display: flex;
118 max-width: calc(100% - 160px);
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700119 overflow: hidden;
120 text-overflow: ellipsis;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000121 }
122
123 .user .message-bubble-container {
124 justify-content: flex-end;
125 }
126
127 .agent .message-bubble-container,
128 .tool .message-bubble-container,
129 .error .message-bubble-container {
130 justify-content: flex-start;
Sean McCullough86b56862025-04-18 13:04:03 -0700131 }
132
133 .message-content {
134 position: relative;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000135 padding: 6px 10px;
136 border-radius: 12px;
137 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700138 max-width: 100%;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000139 width: fit-content;
140 min-width: min-content;
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700141 overflow-wrap: break-word;
142 word-break: break-word;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000143 }
144
145 /* User message styling */
146 .user .message-content {
147 background-color: #2196f3;
148 color: white;
149 border-bottom-right-radius: 5px;
150 }
151
152 /* Agent message styling */
153 .agent .message-content,
154 .tool .message-content,
155 .error .message-content {
156 background-color: #f1f1f1;
157 color: black;
158 border-bottom-left-radius: 5px;
Sean McCullough86b56862025-04-18 13:04:03 -0700159 }
160
161 /* Copy button styles */
162 .message-text-container,
163 .tool-result-container {
164 position: relative;
165 }
166
167 .message-actions {
168 position: absolute;
169 top: 5px;
170 right: 5px;
171 z-index: 10;
172 opacity: 0;
173 transition: opacity 0.2s ease;
174 }
175
176 .message-text-container:hover .message-actions,
177 .tool-result-container:hover .message-actions {
178 opacity: 1;
179 }
180
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000181 .message-actions {
Sean McCullough86b56862025-04-18 13:04:03 -0700182 display: flex;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000183 gap: 6px;
184 }
185
186 .copy-icon,
187 .info-icon {
188 background-color: transparent;
189 border: none;
190 color: rgba(0, 0, 0, 0.6);
191 cursor: pointer;
192 padding: 3px;
193 border-radius: 50%;
194 display: flex;
195 align-items: center;
196 justify-content: center;
197 width: 24px;
198 height: 24px;
199 transition: all 0.15s ease;
200 }
201
202 .user .copy-icon,
203 .user .info-icon {
204 color: rgba(255, 255, 255, 0.8);
205 }
206
207 .copy-icon:hover,
208 .info-icon:hover {
209 background-color: rgba(0, 0, 0, 0.08);
210 }
211
212 .user .copy-icon:hover,
213 .user .info-icon:hover {
214 background-color: rgba(255, 255, 255, 0.15);
215 }
216
217 /* Message metadata styling */
218 .message-type {
219 font-weight: bold;
220 font-size: 11px;
Sean McCullough86b56862025-04-18 13:04:03 -0700221 }
222
223 .message-timestamp {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000224 display: block;
Sean McCullough86b56862025-04-18 13:04:03 -0700225 font-size: 10px;
226 color: #888;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000227 margin-top: 2px;
228 }
229
230 .message-duration {
231 display: block;
232 font-size: 10px;
233 color: #888;
234 margin-top: 2px;
Sean McCullough86b56862025-04-18 13:04:03 -0700235 }
236
237 .message-usage {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000238 display: block;
Sean McCullough86b56862025-04-18 13:04:03 -0700239 font-size: 10px;
240 color: #888;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000241 margin-top: 3px;
Sean McCullough86b56862025-04-18 13:04:03 -0700242 }
243
244 .conversation-id {
245 font-family: monospace;
246 font-size: 12px;
247 padding: 2px 4px;
Sean McCullough86b56862025-04-18 13:04:03 -0700248 margin-left: auto;
249 }
250
251 .parent-info {
252 font-size: 11px;
253 opacity: 0.8;
254 }
255
256 .subconversation {
257 border-left: 2px solid transparent;
258 padding-left: 5px;
259 margin-left: 20px;
260 transition: margin-left 0.3s ease;
261 }
262
263 .message-text {
264 overflow-x: auto;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000265 margin-bottom: 0;
266 font-family: sans-serif;
267 padding: 2px 0;
Sean McCullough86b56862025-04-18 13:04:03 -0700268 user-select: text;
269 cursor: text;
270 -webkit-user-select: text;
271 -moz-user-select: text;
272 -ms-user-select: text;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000273 font-size: 14px;
274 line-height: 1.35;
275 text-align: left;
276 }
277
278 /* Style for code blocks within messages */
279 .message-text pre,
280 .message-text code {
281 font-family: monospace;
282 background: rgba(0, 0, 0, 0.05);
283 border-radius: 4px;
284 padding: 2px 4px;
285 overflow-x: auto;
286 max-width: 100%;
287 white-space: pre-wrap; /* Allow wrapping for very long lines */
288 word-break: break-all; /* Break words at any character */
289 box-sizing: border-box; /* Include padding in width calculation */
290 }
291
Pokey Rulea10f1512025-05-15 13:53:26 +0000292 /* Code block container styles */
293 .code-block-container {
294 position: relative;
295 margin: 8px 0;
296 border-radius: 6px;
297 overflow: hidden;
298 background: rgba(0, 0, 0, 0.05);
299 }
300
301 .user .code-block-container {
302 background: rgba(255, 255, 255, 0.2);
303 }
304
305 .code-block-header {
306 display: flex;
307 justify-content: space-between;
308 align-items: center;
309 padding: 4px 8px;
310 background: rgba(0, 0, 0, 0.1);
311 font-size: 12px;
312 }
313
314 .user .code-block-header {
315 background: rgba(255, 255, 255, 0.2);
316 color: white;
317 }
318
319 .code-language {
320 font-family: monospace;
321 font-size: 11px;
322 font-weight: 500;
323 }
324
325 .code-copy-button {
326 background: transparent;
327 border: none;
328 color: inherit;
329 cursor: pointer;
330 padding: 2px;
331 border-radius: 3px;
332 display: flex;
333 align-items: center;
334 justify-content: center;
335 opacity: 0.7;
336 transition: all 0.15s ease;
337 }
338
339 .code-copy-button:hover {
340 opacity: 1;
341 background: rgba(0, 0, 0, 0.1);
342 }
343
344 .user .code-copy-button:hover {
345 background: rgba(255, 255, 255, 0.2);
346 }
347
348 .code-block-container pre {
349 margin: 0;
350 padding: 8px;
351 background: transparent;
352 }
353
354 .code-block-container code {
355 background: transparent;
356 padding: 0;
357 display: block;
358 width: 100%;
359 }
360
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000361 .user .message-text pre,
362 .user .message-text code {
363 background: rgba(255, 255, 255, 0.2);
364 color: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700365 }
366
367 .tool-details {
368 margin-top: 3px;
369 padding-top: 3px;
370 border-top: 1px dashed #e0e0e0;
371 font-size: 12px;
372 }
373
374 .tool-name {
375 font-size: 12px;
376 font-weight: bold;
377 margin-bottom: 2px;
378 background: #f0f0f0;
379 padding: 2px 4px;
380 border-radius: 2px;
381 display: flex;
382 align-items: center;
383 gap: 3px;
384 }
385
386 .tool-input,
387 .tool-result {
388 margin-top: 2px;
389 padding: 3px 5px;
390 background: #f7f7f7;
391 border-radius: 2px;
392 font-family: monospace;
393 font-size: 12px;
394 overflow-x: auto;
395 white-space: pre;
396 line-height: 1.3;
397 user-select: text;
398 cursor: text;
399 -webkit-user-select: text;
400 -moz-user-select: text;
401 -ms-user-select: text;
402 }
403
404 .tool-result {
405 max-height: 300px;
406 overflow-y: auto;
407 }
408
409 .usage-info {
410 margin-top: 10px;
411 padding-top: 10px;
412 border-top: 1px dashed #e0e0e0;
413 font-size: 12px;
414 color: #666;
415 }
416
417 /* Custom styles for IRC-like experience */
418 .user .message-content {
419 border-left-color: #2196f3;
420 }
421
422 .agent .message-content {
423 border-left-color: #4caf50;
424 }
425
426 .tool .message-content {
427 border-left-color: #ff9800;
428 }
429
430 .error .message-content {
431 border-left-color: #f44336;
432 }
433
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700434 /* Compact message styling - distinct visual separation */
435 .compact {
436 background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
437 border: 2px solid #fd7e14;
438 border-radius: 12px;
439 margin: 20px 0;
440 padding: 0;
441 }
442
443 .compact .message-content {
444 border-left: 4px solid #fd7e14;
445 background: rgba(253, 126, 20, 0.05);
446 font-weight: 500;
447 }
448
449 .compact .message-text {
450 color: #8b4513;
451 font-size: 13px;
452 line-height: 1.4;
453 }
454
455 .compact::before {
456 content: "📚 CONVERSATION EPOCH";
457 display: block;
458 text-align: center;
459 font-size: 11px;
460 font-weight: bold;
461 color: #8b4513;
462 background: #fd7e14;
463 color: white;
464 padding: 4px 8px;
465 margin: 0;
466 border-radius: 8px 8px 0 0;
467 letter-spacing: 1px;
468 }
469
470 /* Pre-compaction messages get a subtle diagonal stripe background */
471 .pre-compaction {
472 background: repeating-linear-gradient(
473 45deg,
474 #ffffff,
475 #ffffff 10px,
476 #f8f8f8 10px,
477 #f8f8f8 20px
478 );
479 opacity: 0.85;
480 border-left: 3px solid #ddd;
481 }
482
483 .pre-compaction .message-content {
484 background: rgba(255, 255, 255, 0.7);
485 backdrop-filter: blur(1px);
Philip Zeyliger57d28bc2025-06-06 20:28:34 +0000486 color: #333; /* Ensure dark text for readability */
487 }
488
489 .pre-compaction .message-text {
490 color: #333; /* Ensure dark text in message content */
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700491 }
492
Sean McCullough86b56862025-04-18 13:04:03 -0700493 /* Make message type display bold but without the IRC-style markers */
494 .message-type {
495 font-weight: bold;
496 }
497
498 /* Commit message styling */
Sean McCullough86b56862025-04-18 13:04:03 -0700499 .commits-container {
500 margin-top: 10px;
Sean McCullough86b56862025-04-18 13:04:03 -0700501 }
502
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000503 .commit-notification {
504 background-color: #e8f5e9;
505 color: #2e7d32;
506 font-weight: 500;
507 font-size: 12px;
508 padding: 6px 10px;
509 border-radius: 10px;
510 margin-bottom: 8px;
511 text-align: center;
512 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
Sean McCullough86b56862025-04-18 13:04:03 -0700513 }
514
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000515 .commit-card {
516 background-color: #f5f5f5;
517 border-radius: 8px;
Sean McCullough86b56862025-04-18 13:04:03 -0700518 overflow: hidden;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000519 margin-bottom: 6px;
520 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
521 padding: 6px 8px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000522 display: flex;
523 align-items: center;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000524 gap: 8px;
Sean McCullough86b56862025-04-18 13:04:03 -0700525 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700526
Sean McCullough86b56862025-04-18 13:04:03 -0700527 .commit-hash {
528 color: #0366d6;
529 font-weight: bold;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000530 font-family: monospace;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000531 cursor: pointer;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000532 text-decoration: none;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000533 background-color: rgba(3, 102, 214, 0.08);
534 padding: 2px 5px;
535 border-radius: 4px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000536 }
537
538 .commit-hash:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000539 background-color: rgba(3, 102, 214, 0.15);
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000540 }
541
542 .commit-branch {
543 color: #28a745;
544 font-weight: 500;
545 cursor: pointer;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000546 font-family: monospace;
547 background-color: rgba(40, 167, 69, 0.08);
548 padding: 2px 5px;
549 border-radius: 4px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000550 }
551
552 .commit-branch:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000553 background-color: rgba(40, 167, 69, 0.15);
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000554 }
555
philip.zeyliger6d3de482025-06-10 19:38:14 -0700556 .commit-branch-container {
557 display: flex;
558 align-items: center;
559 gap: 6px;
560 }
561
562 .commit-branch-container .copy-icon {
563 opacity: 0.7;
564 display: flex;
565 align-items: center;
566 }
567
568 .commit-branch-container .copy-icon svg {
569 vertical-align: middle;
570 }
571
572 .commit-branch-container:hover .copy-icon {
573 opacity: 1;
574 }
575
576 .octocat-link {
577 color: #586069;
578 text-decoration: none;
579 display: flex;
580 align-items: center;
581 transition: color 0.2s ease;
582 }
583
584 .octocat-link:hover {
585 color: #0366d6;
586 }
587
588 .octocat-icon {
589 width: 14px;
590 height: 14px;
591 }
592
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000593 .commit-subject {
594 font-size: 13px;
595 color: #333;
596 flex-grow: 1;
597 overflow: hidden;
598 text-overflow: ellipsis;
599 white-space: nowrap;
Sean McCullough86b56862025-04-18 13:04:03 -0700600 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700601
Sean McCullough86b56862025-04-18 13:04:03 -0700602 .commit-diff-button {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000603 padding: 3px 8px;
604 border: none;
605 border-radius: 4px;
606 background-color: #0366d6;
607 color: white;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000608 font-size: 11px;
Sean McCullough86b56862025-04-18 13:04:03 -0700609 cursor: pointer;
610 transition: all 0.2s ease;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000611 display: block;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000612 margin-left: auto;
Sean McCullough86b56862025-04-18 13:04:03 -0700613 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700614
Sean McCullough86b56862025-04-18 13:04:03 -0700615 .commit-diff-button:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000616 background-color: #0256b4;
Sean McCullough86b56862025-04-18 13:04:03 -0700617 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700618
Sean McCullough86b56862025-04-18 13:04:03 -0700619 /* Tool call cards */
620 .tool-call-cards-container {
621 display: flex;
622 flex-direction: column;
623 gap: 8px;
624 margin-top: 8px;
625 }
626
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000627 /* Error message specific styling */
628 .error .message-content {
629 background-color: #ffebee;
630 border-left: 3px solid #f44336;
Sean McCullough86b56862025-04-18 13:04:03 -0700631 }
632
633 .end-of-turn {
634 margin-bottom: 15px;
635 }
636
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000637 .end-of-turn-indicator {
638 display: block;
639 font-size: 11px;
640 color: #777;
641 padding: 2px 0;
642 margin-top: 8px;
643 text-align: right;
644 font-style: italic;
645 }
646
647 .user .end-of-turn-indicator {
648 color: rgba(255, 255, 255, 0.7);
649 }
650
651 /* Message info panel styling */
652 .message-info-panel {
653 margin-top: 8px;
654 padding: 8px;
655 background-color: rgba(0, 0, 0, 0.03);
656 border-radius: 6px;
657 font-size: 12px;
658 transition: all 0.2s ease;
659 border-left: 2px solid rgba(0, 0, 0, 0.1);
660 }
661
662 .user .message-info-panel {
663 background-color: rgba(255, 255, 255, 0.15);
664 border-left: 2px solid rgba(255, 255, 255, 0.2);
665 }
666
667 .info-row {
668 margin-bottom: 3px;
669 display: flex;
670 }
671
672 .info-label {
673 font-weight: bold;
674 margin-right: 5px;
675 min-width: 60px;
676 }
677
678 .info-value {
679 flex: 1;
680 }
681
682 .conversation-id {
683 font-family: monospace;
Sean McCullough86b56862025-04-18 13:04:03 -0700684 font-size: 10px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000685 word-break: break-all;
Sean McCullough86b56862025-04-18 13:04:03 -0700686 }
687
688 .markdown-content {
689 box-sizing: border-box;
690 min-width: 200px;
691 margin: 0 auto;
692 }
693
694 .markdown-content p {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000695 margin-block-start: 0.3em;
696 margin-block-end: 0.3em;
697 }
698
699 .markdown-content p:first-child {
700 margin-block-start: 0;
701 }
702
703 .markdown-content p:last-child {
704 margin-block-end: 0;
705 }
706
707 /* Styling for markdown elements */
708 .markdown-content a {
709 color: inherit;
710 text-decoration: underline;
711 }
712
713 .user .markdown-content a {
714 color: #fff;
715 text-decoration: underline;
716 }
717
718 .markdown-content ul,
719 .markdown-content ol {
720 padding-left: 1.5em;
721 margin: 0.5em 0;
722 }
723
724 .markdown-content blockquote {
725 border-left: 3px solid rgba(0, 0, 0, 0.2);
726 padding-left: 1em;
727 margin-left: 0.5em;
728 font-style: italic;
729 }
730
731 .user .markdown-content blockquote {
732 border-left: 3px solid rgba(255, 255, 255, 0.4);
Sean McCullough86b56862025-04-18 13:04:03 -0700733 }
Autoformatterdded2d62025-04-28 00:27:21 +0000734
Sean McCullough8d93e362025-04-27 23:32:18 +0000735 /* Mermaid diagram styling */
736 .mermaid-container {
737 margin: 1em 0;
738 padding: 0.5em;
739 background-color: #f8f8f8;
740 border-radius: 4px;
741 overflow-x: auto;
742 }
Autoformatterdded2d62025-04-28 00:27:21 +0000743
Sean McCullough8d93e362025-04-27 23:32:18 +0000744 .mermaid {
745 text-align: center;
746 }
Sean McCullough86b56862025-04-18 13:04:03 -0700747 `;
748
Sean McCullough8d93e362025-04-27 23:32:18 +0000749 // Track mermaid diagrams that need rendering
750 private mermaidDiagrams = new Map();
751
Sean McCullough86b56862025-04-18 13:04:03 -0700752 constructor() {
753 super();
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000754 // Mermaid will be initialized lazily when first needed
Sean McCullough86b56862025-04-18 13:04:03 -0700755 }
756
757 // See https://lit.dev/docs/components/lifecycle/
758 connectedCallback() {
759 super.connectedCallback();
760 }
Autoformatterdded2d62025-04-28 00:27:21 +0000761
Sean McCullough8d93e362025-04-27 23:32:18 +0000762 // After the component is updated and rendered, render any mermaid diagrams
763 updated(changedProperties: Map<string, unknown>) {
764 super.updated(changedProperties);
765 this.renderMermaidDiagrams();
Pokey Rulea10f1512025-05-15 13:53:26 +0000766 this.setupCodeBlockCopyButtons();
Sean McCullough8d93e362025-04-27 23:32:18 +0000767 }
Autoformatterdded2d62025-04-28 00:27:21 +0000768
Sean McCullough8d93e362025-04-27 23:32:18 +0000769 // Render mermaid diagrams after the component is updated
770 renderMermaidDiagrams() {
771 // Add a small delay to ensure the DOM is fully rendered
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000772 setTimeout(async () => {
Sean McCullough8d93e362025-04-27 23:32:18 +0000773 // Find all mermaid containers in our shadow root
Autoformatterdded2d62025-04-28 00:27:21 +0000774 const containers = this.shadowRoot?.querySelectorAll(".mermaid");
Sean McCullough8d93e362025-04-27 23:32:18 +0000775 if (!containers || containers.length === 0) return;
Autoformatterdded2d62025-04-28 00:27:21 +0000776
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000777 try {
778 // Load mermaid dynamically
779 const mermaidLib = await loadMermaid();
Autoformatterdded2d62025-04-28 00:27:21 +0000780
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000781 // Initialize mermaid with specific config (only once per load)
782 mermaidLib.initialize({
783 startOnLoad: false,
784 suppressErrorRendering: true,
785 theme: "default",
786 securityLevel: "loose", // Allows more flexibility but be careful with user-generated content
787 fontFamily: "monospace",
788 });
Autoformatterdded2d62025-04-28 00:27:21 +0000789
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000790 // Process each mermaid diagram
791 containers.forEach((container) => {
792 const id = container.id;
793 const code = container.textContent || "";
794 if (!code || !id) return; // Use return for forEach instead of continue
795
796 try {
797 // Clear any previous content
798 container.innerHTML = code;
799
800 // Render the mermaid diagram using promise
801 mermaidLib
802 .render(`${id}-svg`, code)
803 .then(({ svg }) => {
804 container.innerHTML = svg;
805 })
806 .catch((err) => {
807 console.error("Error rendering mermaid diagram:", err);
808 // Show the original code as fallback
809 container.innerHTML = `<pre>${code}</pre>`;
810 });
811 } catch (err) {
812 console.error("Error processing mermaid diagram:", err);
813 // Show the original code as fallback
814 container.innerHTML = `<pre>${code}</pre>`;
815 }
816 });
817 } catch (err) {
818 console.error("Error loading mermaid:", err);
819 // Show the original code as fallback for all diagrams
820 containers.forEach((container) => {
821 const code = container.textContent || "";
Sean McCullough8d93e362025-04-27 23:32:18 +0000822 container.innerHTML = `<pre>${code}</pre>`;
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000823 });
824 }
Sean McCullough8d93e362025-04-27 23:32:18 +0000825 }, 100); // Small delay to ensure DOM is ready
826 }
Sean McCullough86b56862025-04-18 13:04:03 -0700827
Pokey Rulea10f1512025-05-15 13:53:26 +0000828 // Setup code block copy buttons after component is updated
829 setupCodeBlockCopyButtons() {
830 setTimeout(() => {
831 // Find all copy buttons in code blocks
832 const copyButtons =
833 this.shadowRoot?.querySelectorAll(".code-copy-button");
834 if (!copyButtons || copyButtons.length === 0) return;
835
836 // Add click event listener to each button
837 copyButtons.forEach((button) => {
838 button.addEventListener("click", (e) => {
839 e.stopPropagation();
840 const codeId = (button as HTMLElement).dataset.codeId;
841 if (!codeId) return;
842
843 const codeElement = this.shadowRoot?.querySelector(`#${codeId}`);
844 if (!codeElement) return;
845
846 const codeText = codeElement.textContent || "";
847 const buttonRect = button.getBoundingClientRect();
848
849 // Copy code to clipboard
850 navigator.clipboard
851 .writeText(codeText)
852 .then(() => {
853 // Show success indicator
854 const originalHTML = button.innerHTML;
855 button.innerHTML = `
856 <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">
857 <path d="M20 6L9 17l-5-5"></path>
858 </svg>
859 `;
860
861 // Display floating message
862 this.showFloatingMessage("Copied!", buttonRect, "success");
863
864 // Reset button after delay
865 setTimeout(() => {
866 button.innerHTML = originalHTML;
867 }, 2000);
868 })
869 .catch((err) => {
870 console.error("Failed to copy code:", err);
871 this.showFloatingMessage("Failed to copy!", buttonRect, "error");
872 });
873 });
874 });
875 }, 100); // Small delay to ensure DOM is ready
876 }
877
Sean McCullough86b56862025-04-18 13:04:03 -0700878 // See https://lit.dev/docs/components/lifecycle/
879 disconnectedCallback() {
880 super.disconnectedCallback();
881 }
882
883 renderMarkdown(markdownContent: string): string {
884 try {
Sean McCullough8d93e362025-04-27 23:32:18 +0000885 // Create a custom renderer
886 const renderer = new Renderer();
887 const originalCodeRenderer = renderer.code.bind(renderer);
Autoformatterdded2d62025-04-28 00:27:21 +0000888
Pokey Rulea10f1512025-05-15 13:53:26 +0000889 // Override the code renderer to handle mermaid diagrams and add copy buttons
Autoformatterdded2d62025-04-28 00:27:21 +0000890 renderer.code = function ({ text, lang, escaped }: Tokens.Code): string {
891 if (lang === "mermaid") {
Sean McCullough8d93e362025-04-27 23:32:18 +0000892 // Generate a unique ID for this diagram
893 const id = `mermaid-diagram-${Math.random().toString(36).substring(2, 10)}`;
Autoformatterdded2d62025-04-28 00:27:21 +0000894
Sean McCullough8d93e362025-04-27 23:32:18 +0000895 // Just create the container and mermaid div - we'll render it in the updated() lifecycle method
896 return `<div class="mermaid-container">
897 <div class="mermaid" id="${id}">${text}</div>
898 </div>`;
899 }
Pokey Rulea10f1512025-05-15 13:53:26 +0000900
Philip Zeyliger0d092842025-06-09 18:57:12 -0700901 // For regular code blocks, call the original renderer to get properly escaped HTML
902 const originalCodeHtml = originalCodeRenderer({ text, lang, escaped });
903
904 // Extract the code content from the original HTML to add our custom wrapper
905 // The original renderer returns: <pre><code class="language-x">escapedText</code></pre>
906 const codeMatch = originalCodeHtml.match(
907 /<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/,
908 );
909 if (!codeMatch) {
910 // Fallback to original if we can't parse it
911 return originalCodeHtml;
912 }
913
914 const escapedText = codeMatch[1];
Pokey Rulea10f1512025-05-15 13:53:26 +0000915 const id = `code-block-${Math.random().toString(36).substring(2, 10)}`;
916 const langClass = lang ? ` class="language-${lang}"` : "";
917
918 return `<div class="code-block-container">
919 <div class="code-block-header">
920 ${lang ? `<span class="code-language">${lang}</span>` : ""}
921 <button class="code-copy-button" title="Copy code" data-code-id="${id}">
922 <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">
923 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
924 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
925 </svg>
926 </button>
927 </div>
Philip Zeyliger0d092842025-06-09 18:57:12 -0700928 <pre><code id="${id}"${langClass}>${escapedText}</code></pre>
Pokey Rulea10f1512025-05-15 13:53:26 +0000929 </div>`;
Sean McCullough8d93e362025-04-27 23:32:18 +0000930 };
Autoformatterdded2d62025-04-28 00:27:21 +0000931
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000932 // Set markdown options for proper code block highlighting
Sean McCullough86b56862025-04-18 13:04:03 -0700933 const markedOptions: MarkedOptions = {
934 gfm: true, // GitHub Flavored Markdown
935 breaks: true, // Convert newlines to <br>
936 async: false,
Autoformatterdded2d62025-04-28 00:27:21 +0000937 renderer: renderer,
Sean McCullough86b56862025-04-18 13:04:03 -0700938 };
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000939
940 // Parse markdown and sanitize the output HTML with DOMPurify
941 const htmlOutput = marked.parse(markdownContent, markedOptions) as string;
942 return DOMPurify.sanitize(htmlOutput, {
943 // Allow common HTML elements that are safe
944 ALLOWED_TAGS: [
945 "p",
946 "br",
947 "strong",
948 "em",
949 "b",
950 "i",
951 "u",
952 "s",
953 "code",
954 "pre",
955 "h1",
956 "h2",
957 "h3",
958 "h4",
959 "h5",
960 "h6",
961 "ul",
962 "ol",
963 "li",
964 "blockquote",
965 "a",
966 "div",
967 "span", // For mermaid diagrams and code blocks
968 "svg",
969 "g",
970 "path",
971 "rect",
972 "circle",
973 "text",
974 "line",
975 "polygon", // For mermaid SVG
976 "button", // For code copy buttons
977 ],
978 ALLOWED_ATTR: [
979 "href",
980 "title",
981 "target",
982 "rel", // For links
983 "class",
984 "id", // For styling and functionality
985 "data-*", // For code copy buttons
986 // SVG attributes for mermaid diagrams
987 "viewBox",
988 "width",
989 "height",
990 "xmlns",
991 "fill",
992 "stroke",
993 "stroke-width",
994 "d",
995 "x",
996 "y",
997 "x1",
998 "y1",
999 "x2",
1000 "y2",
1001 "cx",
1002 "cy",
1003 "r",
1004 "rx",
1005 "ry",
1006 "points",
1007 "transform",
1008 "text-anchor",
1009 "font-size",
1010 "font-family",
1011 ],
1012 // Allow data attributes for functionality
1013 ALLOW_DATA_ATTR: true,
1014 // Keep whitespace for code formatting
1015 KEEP_CONTENT: true,
1016 });
Sean McCullough86b56862025-04-18 13:04:03 -07001017 } catch (error) {
1018 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +00001019 // Fallback to sanitized plain text if markdown parsing fails
1020 return DOMPurify.sanitize(markdownContent);
Sean McCullough86b56862025-04-18 13:04:03 -07001021 }
1022 }
1023
1024 /**
1025 * Format timestamp for display
1026 */
1027 formatTimestamp(
1028 timestamp: string | number | Date | null | undefined,
1029 defaultValue: string = "",
1030 ): string {
1031 if (!timestamp) return defaultValue;
1032 try {
1033 const date = new Date(timestamp);
1034 if (isNaN(date.getTime())) return defaultValue;
1035
1036 // Format: Mar 13, 2025 09:53:25 AM
1037 return date.toLocaleString("en-US", {
1038 month: "short",
1039 day: "numeric",
1040 year: "numeric",
1041 hour: "numeric",
1042 minute: "2-digit",
1043 second: "2-digit",
1044 hour12: true,
1045 });
1046 } catch (e) {
1047 return defaultValue;
1048 }
1049 }
1050
1051 formatNumber(
1052 num: number | null | undefined,
1053 defaultValue: string = "0",
1054 ): string {
1055 if (num === undefined || num === null) return defaultValue;
1056 try {
1057 return num.toLocaleString();
1058 } catch (e) {
1059 return String(num);
1060 }
1061 }
1062 formatCurrency(
1063 num: number | string | null | undefined,
1064 defaultValue: string = "$0.00",
1065 isMessageLevel: boolean = false,
1066 ): string {
1067 if (num === undefined || num === null) return defaultValue;
1068 try {
1069 // Use 4 decimal places for message-level costs, 2 for totals
1070 const decimalPlaces = isMessageLevel ? 4 : 2;
1071 return `$${parseFloat(String(num)).toFixed(decimalPlaces)}`;
1072 } catch (e) {
1073 return defaultValue;
1074 }
1075 }
1076
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001077 // Format duration from nanoseconds to a human-readable string
1078 _formatDuration(nanoseconds: number | null | undefined): string {
1079 if (!nanoseconds) return "0s";
1080
1081 const seconds = nanoseconds / 1e9;
1082
1083 if (seconds < 60) {
1084 return `${seconds.toFixed(1)}s`;
1085 } else if (seconds < 3600) {
1086 const minutes = Math.floor(seconds / 60);
1087 const remainingSeconds = seconds % 60;
1088 return `${minutes}min ${remainingSeconds.toFixed(0)}s`;
1089 } else {
1090 const hours = Math.floor(seconds / 3600);
1091 const remainingSeconds = seconds % 3600;
1092 const minutes = Math.floor(remainingSeconds / 60);
1093 return `${hours}h ${minutes}min`;
1094 }
1095 }
1096
Sean McCullough86b56862025-04-18 13:04:03 -07001097 showCommit(commitHash: string) {
Sean McCullough71941bd2025-04-18 13:31:48 -07001098 this.dispatchEvent(
1099 new CustomEvent("show-commit-diff", {
1100 bubbles: true,
1101 composed: true,
1102 detail: { commitHash },
1103 }),
1104 );
Sean McCullough86b56862025-04-18 13:04:03 -07001105 }
1106
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001107 _toggleInfo(e: Event) {
1108 e.stopPropagation();
1109 this.showInfo = !this.showInfo;
1110 }
1111
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001112 copyToClipboard(text: string, event: Event) {
1113 const element = event.currentTarget as HTMLElement;
1114 const rect = element.getBoundingClientRect();
1115
1116 navigator.clipboard
1117 .writeText(text)
1118 .then(() => {
1119 this.showFloatingMessage("Copied!", rect, "success");
1120 })
1121 .catch((err) => {
1122 console.error("Failed to copy text: ", err);
1123 this.showFloatingMessage("Failed to copy!", rect, "error");
1124 });
1125 }
1126
1127 showFloatingMessage(
1128 message: string,
1129 targetRect: DOMRect,
1130 type: "success" | "error",
1131 ) {
1132 // Create floating message element
1133 const floatingMsg = document.createElement("div");
1134 floatingMsg.textContent = message;
1135 floatingMsg.className = `floating-message ${type}`;
1136
1137 // Position it near the clicked element
1138 // Position just above the element
1139 const top = targetRect.top - 30;
1140 const left = targetRect.left + targetRect.width / 2 - 40;
1141
1142 floatingMsg.style.position = "fixed";
1143 floatingMsg.style.top = `${top}px`;
1144 floatingMsg.style.left = `${left}px`;
1145 floatingMsg.style.zIndex = "9999";
1146
1147 // Add to document body
1148 document.body.appendChild(floatingMsg);
1149
1150 // Animate in
1151 floatingMsg.style.opacity = "0";
1152 floatingMsg.style.transform = "translateY(10px)";
1153
1154 setTimeout(() => {
1155 floatingMsg.style.opacity = "1";
1156 floatingMsg.style.transform = "translateY(0)";
1157 }, 10);
1158
1159 // Remove after animation
1160 setTimeout(() => {
1161 floatingMsg.style.opacity = "0";
1162 floatingMsg.style.transform = "translateY(-10px)";
1163
1164 setTimeout(() => {
1165 document.body.removeChild(floatingMsg);
1166 }, 300);
1167 }, 1500);
1168 }
1169
philip.zeyliger6d3de482025-06-10 19:38:14 -07001170 // Format GitHub repository URL to org/repo format
1171 formatGitHubRepo(url) {
1172 if (!url) return null;
1173
1174 // Common GitHub URL patterns
1175 const patterns = [
1176 // HTTPS URLs
1177 /https:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
1178 // SSH URLs
1179 /git@github\.com:([^/]+)\/([^/\s.]+)(?:\.git)?/,
1180 // Git protocol
1181 /git:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
1182 ];
1183
1184 for (const pattern of patterns) {
1185 const match = url.match(pattern);
1186 if (match) {
1187 return {
1188 formatted: `${match[1]}/${match[2]}`,
1189 url: `https://github.com/${match[1]}/${match[2]}`,
1190 owner: match[1],
1191 repo: match[2],
1192 };
1193 }
1194 }
1195
1196 return null;
1197 }
1198
1199 // Generate GitHub branch URL if linking is enabled
1200 getGitHubBranchLink(branchName) {
1201 if (!this.state?.link_to_github || !branchName) {
1202 return null;
1203 }
1204
1205 const github = this.formatGitHubRepo(this.state?.git_origin);
1206 if (!github) {
1207 return null;
1208 }
1209
1210 return `https://github.com/${github.owner}/${github.repo}/tree/${branchName}`;
1211 }
1212
Sean McCullough86b56862025-04-18 13:04:03 -07001213 render() {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001214 // Calculate if this is an end of turn message with no parent conversation ID
1215 const isEndOfTurn =
1216 this.message?.end_of_turn && !this.message?.parent_conversation_id;
1217
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001218 const isPreCompaction =
1219 this.message?.idx !== undefined &&
1220 this.message.idx < this.firstMessageIndex;
1221
Sean McCullough86b56862025-04-18 13:04:03 -07001222 return html`
1223 <div
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001224 class="message ${this.message?.type} ${isEndOfTurn
Sean McCullough86b56862025-04-18 13:04:03 -07001225 ? "end-of-turn"
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001226 : ""} ${isPreCompaction ? "pre-compaction" : ""}"
Sean McCullough86b56862025-04-18 13:04:03 -07001227 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001228 <div class="message-container">
1229 <!-- Left area (empty for simplicity) -->
1230 <div class="message-metadata-left"></div>
1231
1232 <!-- Message bubble -->
1233 <div class="message-bubble-container">
1234 <div class="message-content">
1235 <div class="message-text-container">
1236 <div class="message-actions">
1237 ${copyButton(this.message?.content)}
1238 <button
1239 class="info-icon"
1240 title="Show message details"
1241 @click=${this._toggleInfo}
Sean McCullough71941bd2025-04-18 13:31:48 -07001242 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001243 <svg
1244 xmlns="http://www.w3.org/2000/svg"
1245 width="16"
1246 height="16"
1247 viewBox="0 0 24 24"
1248 fill="none"
1249 stroke="currentColor"
1250 stroke-width="2"
1251 stroke-linecap="round"
1252 stroke-linejoin="round"
1253 >
1254 <circle cx="12" cy="12" r="10"></circle>
1255 <line x1="12" y1="16" x2="12" y2="12"></line>
1256 <line x1="12" y1="8" x2="12.01" y2="8"></line>
1257 </svg>
1258 </button>
1259 </div>
1260 ${this.message?.content
1261 ? html`
1262 <div class="message-text markdown-content">
1263 ${unsafeHTML(
1264 this.renderMarkdown(this.message?.content),
1265 )}
1266 </div>
1267 `
1268 : ""}
1269
1270 <!-- End of turn indicator inside the bubble -->
1271 ${isEndOfTurn && this.message?.elapsed
1272 ? html`
1273 <div class="end-of-turn-indicator">
1274 end of turn
1275 (${this._formatDuration(this.message?.elapsed)})
1276 </div>
1277 `
1278 : ""}
1279
1280 <!-- Info panel that can be toggled -->
1281 ${this.showInfo
1282 ? html`
1283 <div class="message-info-panel">
1284 <div class="info-row">
1285 <span class="info-label">Type:</span>
1286 <span class="info-value">${this.message?.type}</span>
1287 </div>
1288 <div class="info-row">
1289 <span class="info-label">Time:</span>
1290 <span class="info-value"
1291 >${this.formatTimestamp(
1292 this.message?.timestamp,
1293 "",
1294 )}</span
1295 >
1296 </div>
1297 ${this.message?.elapsed
1298 ? html`
1299 <div class="info-row">
1300 <span class="info-label">Duration:</span>
1301 <span class="info-value"
1302 >${this._formatDuration(
1303 this.message?.elapsed,
1304 )}</span
1305 >
1306 </div>
1307 `
1308 : ""}
1309 ${this.message?.usage
1310 ? html`
1311 <div class="info-row">
1312 <span class="info-label">Tokens:</span>
1313 <span class="info-value">
1314 ${this.message?.usage
1315 ? html`
1316 <div>
1317 Input:
1318 ${this.formatNumber(
1319 this.message?.usage?.input_tokens ||
1320 0,
1321 )}
1322 </div>
1323 ${this.message?.usage
1324 ?.cache_creation_input_tokens
1325 ? html`
1326 <div>
1327 Cache creation:
1328 ${this.formatNumber(
1329 this.message?.usage
1330 ?.cache_creation_input_tokens,
1331 )}
1332 </div>
1333 `
1334 : ""}
1335 ${this.message?.usage
1336 ?.cache_read_input_tokens
1337 ? html`
1338 <div>
1339 Cache read:
1340 ${this.formatNumber(
1341 this.message?.usage
1342 ?.cache_read_input_tokens,
1343 )}
1344 </div>
1345 `
1346 : ""}
1347 <div>
1348 Output:
1349 ${this.formatNumber(
1350 this.message?.usage?.output_tokens,
1351 )}
1352 </div>
1353 <div>
1354 Cost:
1355 ${this.formatCurrency(
1356 this.message?.usage?.cost_usd,
1357 )}
1358 </div>
1359 `
1360 : "N/A"}
1361 </span>
1362 </div>
1363 `
1364 : ""}
1365 ${this.message?.conversation_id
1366 ? html`
1367 <div class="info-row">
1368 <span class="info-label">Conversation ID:</span>
1369 <span class="info-value conversation-id"
1370 >${this.message?.conversation_id}</span
1371 >
1372 </div>
1373 `
1374 : ""}
1375 </div>
1376 `
1377 : ""}
1378 </div>
1379
1380 <!-- Tool calls - only shown for agent messages -->
1381 ${this.message?.type === "agent"
1382 ? html`
1383 <sketch-tool-calls
1384 .toolCalls=${this.message?.tool_calls}
1385 .open=${this.open}
1386 ></sketch-tool-calls>
1387 `
1388 : ""}
1389
1390 <!-- Commits section (redesigned as bubbles) -->
1391 ${this.message?.commits
1392 ? html`
1393 <div class="commits-container">
1394 <div class="commit-notification">
1395 ${this.message.commits.length} new
1396 commit${this.message.commits.length > 1 ? "s" : ""}
1397 detected
1398 </div>
1399 ${this.message.commits.map((commit) => {
1400 return html`
1401 <div class="commit-card">
Philip Zeyliger72682df2025-04-23 13:09:46 -07001402 <span
1403 class="commit-hash"
1404 title="Click to copy: ${commit.hash}"
1405 @click=${(e) =>
1406 this.copyToClipboard(
1407 commit.hash.substring(0, 8),
1408 e,
1409 )}
1410 >
Pokey Rule7be879f2025-04-23 15:30:15 +01001411 ${commit.hash.substring(0, 8)}
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001412 </span>
1413 ${commit.pushed_branch
philip.zeyliger6d3de482025-06-10 19:38:14 -07001414 ? (() => {
1415 const githubLink = this.getGitHubBranchLink(
1416 commit.pushed_branch,
1417 );
1418 return html`
1419 <div class="commit-branch-container">
1420 <span
1421 class="commit-branch pushed-branch"
1422 title="Click to copy: ${commit.pushed_branch}"
1423 @click=${(e) =>
1424 this.copyToClipboard(
1425 commit.pushed_branch,
1426 e,
1427 )}
1428 >${commit.pushed_branch}</span
1429 >
1430 <span class="copy-icon">
1431 <svg
1432 xmlns="http://www.w3.org/2000/svg"
1433 width="14"
1434 height="14"
1435 viewBox="0 0 24 24"
1436 fill="none"
1437 stroke="currentColor"
1438 stroke-width="2"
1439 stroke-linecap="round"
1440 stroke-linejoin="round"
1441 >
1442 <rect
1443 x="9"
1444 y="9"
1445 width="13"
1446 height="13"
1447 rx="2"
1448 ry="2"
1449 ></rect>
1450 <path
1451 d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
1452 ></path>
1453 </svg>
1454 </span>
1455 ${githubLink
1456 ? html`
1457 <a
1458 href="${githubLink}"
1459 target="_blank"
1460 rel="noopener noreferrer"
1461 class="octocat-link"
1462 title="Open ${commit.pushed_branch} on GitHub"
1463 @click=${(e) =>
1464 e.stopPropagation()}
1465 >
1466 <svg
1467 class="octocat-icon"
1468 viewBox="0 0 16 16"
1469 width="14"
1470 height="14"
1471 >
1472 <path
1473 fill="currentColor"
1474 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"
1475 />
1476 </svg>
1477 </a>
1478 `
1479 : ""}
1480 </div>
1481 `;
1482 })()
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001483 : ``}
1484 <span class="commit-subject"
1485 >${commit.subject}</span
Sean McCullough71941bd2025-04-18 13:31:48 -07001486 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001487 <button
1488 class="commit-diff-button"
1489 @click=${() => this.showCommit(commit.hash)}
1490 >
1491 View Diff
1492 </button>
Sean McCullough86b56862025-04-18 13:04:03 -07001493 </div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001494 `;
1495 })}
1496 </div>
1497 `
1498 : ""}
1499 </div>
1500 </div>
1501
1502 <!-- Right side (empty for consistency) -->
1503 <div class="message-metadata-right"></div>
Sean McCullough86b56862025-04-18 13:04:03 -07001504 </div>
1505 </div>
1506 `;
1507 }
1508}
1509
Sean McCullough71941bd2025-04-18 13:31:48 -07001510function copyButton(textToCopy: string) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001511 // Use an icon of overlapping rectangles for copy
1512 const buttonClass = "copy-icon";
1513
1514 // SVG for copy icon (two overlapping rectangles)
1515 const copyIcon = html`<svg
1516 xmlns="http://www.w3.org/2000/svg"
1517 width="16"
1518 height="16"
1519 viewBox="0 0 24 24"
1520 fill="none"
1521 stroke="currentColor"
1522 stroke-width="2"
1523 stroke-linecap="round"
1524 stroke-linejoin="round"
1525 >
1526 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1527 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1528 </svg>`;
1529
1530 // SVG for success check mark
1531 const successIcon = html`<svg
1532 xmlns="http://www.w3.org/2000/svg"
1533 width="16"
1534 height="16"
1535 viewBox="0 0 24 24"
1536 fill="none"
1537 stroke="currentColor"
1538 stroke-width="2"
1539 stroke-linecap="round"
1540 stroke-linejoin="round"
1541 >
1542 <path d="M20 6L9 17l-5-5"></path>
1543 </svg>`;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001544
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001545 const ret = html`<button
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001546 class="${buttonClass}"
1547 title="Copy to clipboard"
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001548 @click=${(e: Event) => {
1549 e.stopPropagation();
1550 const copyButton = e.currentTarget as HTMLButtonElement;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001551 const originalInnerHTML = copyButton.innerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001552 navigator.clipboard
1553 .writeText(textToCopy)
1554 .then(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001555 copyButton.innerHTML = "";
1556 const successElement = document.createElement("div");
1557 copyButton.appendChild(successElement);
1558 render(successIcon, successElement);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001559 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001560 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001561 }, 2000);
1562 })
1563 .catch((err) => {
1564 console.error("Failed to copy text: ", err);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001565 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001566 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001567 }, 2000);
1568 });
1569 }}
1570 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001571 ${copyIcon}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001572 </button>`;
Sean McCullough86b56862025-04-18 13:04:03 -07001573
Sean McCullough71941bd2025-04-18 13:31:48 -07001574 return ret;
Sean McCullough86b56862025-04-18 13:04:03 -07001575}
1576
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001577// Create global styles for floating messages
1578const floatingMessageStyles = document.createElement("style");
1579floatingMessageStyles.textContent = `
1580 .floating-message {
1581 background-color: rgba(0, 0, 0, 0.8);
1582 color: white;
1583 padding: 5px 10px;
1584 border-radius: 4px;
1585 font-size: 12px;
1586 font-family: system-ui, sans-serif;
1587 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
1588 pointer-events: none;
1589 transition: opacity 0.3s ease, transform 0.3s ease;
1590 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001591
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001592 .floating-message.success {
1593 background-color: rgba(40, 167, 69, 0.9);
1594 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001595
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001596 .floating-message.error {
1597 background-color: rgba(220, 53, 69, 0.9);
1598 }
Philip Zeyligere31d2a92025-05-11 15:22:35 -07001599
1600 /* Style for code, pre elements, and tool components to ensure proper wrapping/truncation */
1601 pre, code, sketch-tool-calls, sketch-tool-card, sketch-tool-card-bash {
1602 white-space: nowrap;
1603 overflow: hidden;
1604 text-overflow: ellipsis;
1605 max-width: 100%;
1606 }
1607
1608 /* Special rule for the message content container */
1609 .message-content {
1610 max-width: 100% !important;
1611 overflow: hidden !important;
1612 }
1613
1614 /* Ensure tool call containers don't overflow */
1615 ::slotted(sketch-tool-calls) {
1616 max-width: 100%;
1617 width: 100%;
1618 overflow-wrap: break-word;
1619 word-break: break-word;
1620 }
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001621`;
1622document.head.appendChild(floatingMessageStyles);
1623
Sean McCullough86b56862025-04-18 13:04:03 -07001624declare global {
1625 interface HTMLElementTagNameMap {
1626 "sketch-timeline-message": SketchTimelineMessage;
1627 }
1628}