blob: 37bd4ceed34b358503bffd445af9320a7930e718 [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 }
philip.zeyligerffa94c62025-06-19 18:43:37 -0700747
748 /* Print styles for message components */
749 @media print {
750 .message {
751 page-break-inside: avoid;
752 margin-bottom: 12px;
753 }
754
755 .message-container {
756 page-break-inside: avoid;
757 }
758
759 /* Hide copy buttons and interactive elements during printing */
760 .copy-icon,
761 .info-icon,
762 .commit-diff-button {
763 display: none !important;
764 }
765
766 /* Ensure code blocks print properly */
767 .message-content pre {
768 white-space: pre-wrap;
769 word-wrap: break-word;
770 page-break-inside: avoid;
771 background: #f8f8f8 !important;
772 border: 1px solid #ddd !important;
773 padding: 8px !important;
774 }
775
776 /* Ensure tool calls section prints properly */
777 .tool-calls-section {
778 page-break-inside: avoid;
779 }
780
781 /* Simplify message metadata for print */
782 .message-metadata-left {
783 font-size: 10px;
784 }
785
786 /* Ensure content doesn't break poorly */
787 .message-content {
788 orphans: 3;
789 widows: 3;
790 }
791
792 /* Hide floating messages during print */
793 .floating-message {
794 display: none !important;
795 }
796 }
Sean McCullough86b56862025-04-18 13:04:03 -0700797 `;
798
Sean McCullough8d93e362025-04-27 23:32:18 +0000799 // Track mermaid diagrams that need rendering
800 private mermaidDiagrams = new Map();
801
Sean McCullough86b56862025-04-18 13:04:03 -0700802 constructor() {
803 super();
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000804 // Mermaid will be initialized lazily when first needed
Sean McCullough86b56862025-04-18 13:04:03 -0700805 }
806
807 // See https://lit.dev/docs/components/lifecycle/
808 connectedCallback() {
809 super.connectedCallback();
810 }
Autoformatterdded2d62025-04-28 00:27:21 +0000811
Sean McCullough8d93e362025-04-27 23:32:18 +0000812 // After the component is updated and rendered, render any mermaid diagrams
813 updated(changedProperties: Map<string, unknown>) {
814 super.updated(changedProperties);
815 this.renderMermaidDiagrams();
Pokey Rulea10f1512025-05-15 13:53:26 +0000816 this.setupCodeBlockCopyButtons();
Sean McCullough8d93e362025-04-27 23:32:18 +0000817 }
Autoformatterdded2d62025-04-28 00:27:21 +0000818
Sean McCullough8d93e362025-04-27 23:32:18 +0000819 // Render mermaid diagrams after the component is updated
820 renderMermaidDiagrams() {
821 // Add a small delay to ensure the DOM is fully rendered
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000822 setTimeout(async () => {
Sean McCullough8d93e362025-04-27 23:32:18 +0000823 // Find all mermaid containers in our shadow root
Autoformatterdded2d62025-04-28 00:27:21 +0000824 const containers = this.shadowRoot?.querySelectorAll(".mermaid");
Sean McCullough8d93e362025-04-27 23:32:18 +0000825 if (!containers || containers.length === 0) return;
Autoformatterdded2d62025-04-28 00:27:21 +0000826
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000827 try {
828 // Load mermaid dynamically
829 const mermaidLib = await loadMermaid();
Autoformatterdded2d62025-04-28 00:27:21 +0000830
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000831 // Initialize mermaid with specific config (only once per load)
832 mermaidLib.initialize({
833 startOnLoad: false,
834 suppressErrorRendering: true,
835 theme: "default",
836 securityLevel: "loose", // Allows more flexibility but be careful with user-generated content
837 fontFamily: "monospace",
838 });
Autoformatterdded2d62025-04-28 00:27:21 +0000839
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000840 // Process each mermaid diagram
841 containers.forEach((container) => {
842 const id = container.id;
843 const code = container.textContent || "";
844 if (!code || !id) return; // Use return for forEach instead of continue
845
846 try {
847 // Clear any previous content
848 container.innerHTML = code;
849
850 // Render the mermaid diagram using promise
851 mermaidLib
852 .render(`${id}-svg`, code)
853 .then(({ svg }) => {
854 container.innerHTML = svg;
855 })
856 .catch((err) => {
857 console.error("Error rendering mermaid diagram:", err);
858 // Show the original code as fallback
859 container.innerHTML = `<pre>${code}</pre>`;
860 });
861 } catch (err) {
862 console.error("Error processing mermaid diagram:", err);
863 // Show the original code as fallback
864 container.innerHTML = `<pre>${code}</pre>`;
865 }
866 });
867 } catch (err) {
868 console.error("Error loading mermaid:", err);
869 // Show the original code as fallback for all diagrams
870 containers.forEach((container) => {
871 const code = container.textContent || "";
Sean McCullough8d93e362025-04-27 23:32:18 +0000872 container.innerHTML = `<pre>${code}</pre>`;
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000873 });
874 }
Sean McCullough8d93e362025-04-27 23:32:18 +0000875 }, 100); // Small delay to ensure DOM is ready
876 }
Sean McCullough86b56862025-04-18 13:04:03 -0700877
Pokey Rulea10f1512025-05-15 13:53:26 +0000878 // Setup code block copy buttons after component is updated
879 setupCodeBlockCopyButtons() {
880 setTimeout(() => {
881 // Find all copy buttons in code blocks
882 const copyButtons =
883 this.shadowRoot?.querySelectorAll(".code-copy-button");
884 if (!copyButtons || copyButtons.length === 0) return;
885
886 // Add click event listener to each button
887 copyButtons.forEach((button) => {
888 button.addEventListener("click", (e) => {
889 e.stopPropagation();
890 const codeId = (button as HTMLElement).dataset.codeId;
891 if (!codeId) return;
892
893 const codeElement = this.shadowRoot?.querySelector(`#${codeId}`);
894 if (!codeElement) return;
895
896 const codeText = codeElement.textContent || "";
897 const buttonRect = button.getBoundingClientRect();
898
899 // Copy code to clipboard
900 navigator.clipboard
901 .writeText(codeText)
902 .then(() => {
903 // Show success indicator
904 const originalHTML = button.innerHTML;
905 button.innerHTML = `
906 <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">
907 <path d="M20 6L9 17l-5-5"></path>
908 </svg>
909 `;
910
911 // Display floating message
912 this.showFloatingMessage("Copied!", buttonRect, "success");
913
914 // Reset button after delay
915 setTimeout(() => {
916 button.innerHTML = originalHTML;
917 }, 2000);
918 })
919 .catch((err) => {
920 console.error("Failed to copy code:", err);
921 this.showFloatingMessage("Failed to copy!", buttonRect, "error");
922 });
923 });
924 });
925 }, 100); // Small delay to ensure DOM is ready
926 }
927
Sean McCullough86b56862025-04-18 13:04:03 -0700928 // See https://lit.dev/docs/components/lifecycle/
929 disconnectedCallback() {
930 super.disconnectedCallback();
931 }
932
933 renderMarkdown(markdownContent: string): string {
934 try {
Sean McCullough8d93e362025-04-27 23:32:18 +0000935 // Create a custom renderer
936 const renderer = new Renderer();
937 const originalCodeRenderer = renderer.code.bind(renderer);
Autoformatterdded2d62025-04-28 00:27:21 +0000938
Pokey Rulea10f1512025-05-15 13:53:26 +0000939 // Override the code renderer to handle mermaid diagrams and add copy buttons
Autoformatterdded2d62025-04-28 00:27:21 +0000940 renderer.code = function ({ text, lang, escaped }: Tokens.Code): string {
941 if (lang === "mermaid") {
Sean McCullough8d93e362025-04-27 23:32:18 +0000942 // Generate a unique ID for this diagram
943 const id = `mermaid-diagram-${Math.random().toString(36).substring(2, 10)}`;
Autoformatterdded2d62025-04-28 00:27:21 +0000944
Sean McCullough8d93e362025-04-27 23:32:18 +0000945 // Just create the container and mermaid div - we'll render it in the updated() lifecycle method
946 return `<div class="mermaid-container">
947 <div class="mermaid" id="${id}">${text}</div>
948 </div>`;
949 }
Pokey Rulea10f1512025-05-15 13:53:26 +0000950
Philip Zeyliger0d092842025-06-09 18:57:12 -0700951 // For regular code blocks, call the original renderer to get properly escaped HTML
952 const originalCodeHtml = originalCodeRenderer({ text, lang, escaped });
953
954 // Extract the code content from the original HTML to add our custom wrapper
955 // The original renderer returns: <pre><code class="language-x">escapedText</code></pre>
956 const codeMatch = originalCodeHtml.match(
957 /<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/,
958 );
959 if (!codeMatch) {
960 // Fallback to original if we can't parse it
961 return originalCodeHtml;
962 }
963
964 const escapedText = codeMatch[1];
Pokey Rulea10f1512025-05-15 13:53:26 +0000965 const id = `code-block-${Math.random().toString(36).substring(2, 10)}`;
966 const langClass = lang ? ` class="language-${lang}"` : "";
967
968 return `<div class="code-block-container">
969 <div class="code-block-header">
970 ${lang ? `<span class="code-language">${lang}</span>` : ""}
971 <button class="code-copy-button" title="Copy code" data-code-id="${id}">
972 <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">
973 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
974 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
975 </svg>
976 </button>
977 </div>
Philip Zeyliger0d092842025-06-09 18:57:12 -0700978 <pre><code id="${id}"${langClass}>${escapedText}</code></pre>
Pokey Rulea10f1512025-05-15 13:53:26 +0000979 </div>`;
Sean McCullough8d93e362025-04-27 23:32:18 +0000980 };
Autoformatterdded2d62025-04-28 00:27:21 +0000981
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000982 // Set markdown options for proper code block highlighting
Sean McCullough86b56862025-04-18 13:04:03 -0700983 const markedOptions: MarkedOptions = {
984 gfm: true, // GitHub Flavored Markdown
985 breaks: true, // Convert newlines to <br>
986 async: false,
Autoformatterdded2d62025-04-28 00:27:21 +0000987 renderer: renderer,
Sean McCullough86b56862025-04-18 13:04:03 -0700988 };
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000989
990 // Parse markdown and sanitize the output HTML with DOMPurify
991 const htmlOutput = marked.parse(markdownContent, markedOptions) as string;
992 return DOMPurify.sanitize(htmlOutput, {
993 // Allow common HTML elements that are safe
994 ALLOWED_TAGS: [
995 "p",
996 "br",
997 "strong",
998 "em",
999 "b",
1000 "i",
1001 "u",
1002 "s",
1003 "code",
1004 "pre",
1005 "h1",
1006 "h2",
1007 "h3",
1008 "h4",
1009 "h5",
1010 "h6",
1011 "ul",
1012 "ol",
1013 "li",
1014 "blockquote",
1015 "a",
1016 "div",
1017 "span", // For mermaid diagrams and code blocks
1018 "svg",
1019 "g",
1020 "path",
1021 "rect",
1022 "circle",
1023 "text",
1024 "line",
1025 "polygon", // For mermaid SVG
1026 "button", // For code copy buttons
1027 ],
1028 ALLOWED_ATTR: [
1029 "href",
1030 "title",
1031 "target",
1032 "rel", // For links
1033 "class",
1034 "id", // For styling and functionality
1035 "data-*", // For code copy buttons
1036 // SVG attributes for mermaid diagrams
1037 "viewBox",
1038 "width",
1039 "height",
1040 "xmlns",
1041 "fill",
1042 "stroke",
1043 "stroke-width",
1044 "d",
1045 "x",
1046 "y",
1047 "x1",
1048 "y1",
1049 "x2",
1050 "y2",
1051 "cx",
1052 "cy",
1053 "r",
1054 "rx",
1055 "ry",
1056 "points",
1057 "transform",
1058 "text-anchor",
1059 "font-size",
1060 "font-family",
1061 ],
1062 // Allow data attributes for functionality
1063 ALLOW_DATA_ATTR: true,
1064 // Keep whitespace for code formatting
1065 KEEP_CONTENT: true,
1066 });
Sean McCullough86b56862025-04-18 13:04:03 -07001067 } catch (error) {
1068 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +00001069 // Fallback to sanitized plain text if markdown parsing fails
1070 return DOMPurify.sanitize(markdownContent);
Sean McCullough86b56862025-04-18 13:04:03 -07001071 }
1072 }
1073
1074 /**
1075 * Format timestamp for display
1076 */
1077 formatTimestamp(
1078 timestamp: string | number | Date | null | undefined,
1079 defaultValue: string = "",
1080 ): string {
1081 if (!timestamp) return defaultValue;
1082 try {
1083 const date = new Date(timestamp);
1084 if (isNaN(date.getTime())) return defaultValue;
1085
1086 // Format: Mar 13, 2025 09:53:25 AM
1087 return date.toLocaleString("en-US", {
1088 month: "short",
1089 day: "numeric",
1090 year: "numeric",
1091 hour: "numeric",
1092 minute: "2-digit",
1093 second: "2-digit",
1094 hour12: true,
1095 });
1096 } catch (e) {
1097 return defaultValue;
1098 }
1099 }
1100
1101 formatNumber(
1102 num: number | null | undefined,
1103 defaultValue: string = "0",
1104 ): string {
1105 if (num === undefined || num === null) return defaultValue;
1106 try {
1107 return num.toLocaleString();
1108 } catch (e) {
1109 return String(num);
1110 }
1111 }
1112 formatCurrency(
1113 num: number | string | null | undefined,
1114 defaultValue: string = "$0.00",
1115 isMessageLevel: boolean = false,
1116 ): string {
1117 if (num === undefined || num === null) return defaultValue;
1118 try {
1119 // Use 4 decimal places for message-level costs, 2 for totals
1120 const decimalPlaces = isMessageLevel ? 4 : 2;
1121 return `$${parseFloat(String(num)).toFixed(decimalPlaces)}`;
1122 } catch (e) {
1123 return defaultValue;
1124 }
1125 }
1126
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001127 // Format duration from nanoseconds to a human-readable string
1128 _formatDuration(nanoseconds: number | null | undefined): string {
1129 if (!nanoseconds) return "0s";
1130
1131 const seconds = nanoseconds / 1e9;
1132
1133 if (seconds < 60) {
1134 return `${seconds.toFixed(1)}s`;
1135 } else if (seconds < 3600) {
1136 const minutes = Math.floor(seconds / 60);
1137 const remainingSeconds = seconds % 60;
1138 return `${minutes}min ${remainingSeconds.toFixed(0)}s`;
1139 } else {
1140 const hours = Math.floor(seconds / 3600);
1141 const remainingSeconds = seconds % 3600;
1142 const minutes = Math.floor(remainingSeconds / 60);
1143 return `${hours}h ${minutes}min`;
1144 }
1145 }
1146
Sean McCullough86b56862025-04-18 13:04:03 -07001147 showCommit(commitHash: string) {
Sean McCullough71941bd2025-04-18 13:31:48 -07001148 this.dispatchEvent(
1149 new CustomEvent("show-commit-diff", {
1150 bubbles: true,
1151 composed: true,
1152 detail: { commitHash },
1153 }),
1154 );
Sean McCullough86b56862025-04-18 13:04:03 -07001155 }
1156
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001157 _toggleInfo(e: Event) {
1158 e.stopPropagation();
1159 this.showInfo = !this.showInfo;
1160 }
1161
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001162 copyToClipboard(text: string, event: Event) {
1163 const element = event.currentTarget as HTMLElement;
1164 const rect = element.getBoundingClientRect();
1165
1166 navigator.clipboard
1167 .writeText(text)
1168 .then(() => {
1169 this.showFloatingMessage("Copied!", rect, "success");
1170 })
1171 .catch((err) => {
1172 console.error("Failed to copy text: ", err);
1173 this.showFloatingMessage("Failed to copy!", rect, "error");
1174 });
1175 }
1176
1177 showFloatingMessage(
1178 message: string,
1179 targetRect: DOMRect,
1180 type: "success" | "error",
1181 ) {
1182 // Create floating message element
1183 const floatingMsg = document.createElement("div");
1184 floatingMsg.textContent = message;
1185 floatingMsg.className = `floating-message ${type}`;
1186
1187 // Position it near the clicked element
1188 // Position just above the element
1189 const top = targetRect.top - 30;
1190 const left = targetRect.left + targetRect.width / 2 - 40;
1191
1192 floatingMsg.style.position = "fixed";
1193 floatingMsg.style.top = `${top}px`;
1194 floatingMsg.style.left = `${left}px`;
1195 floatingMsg.style.zIndex = "9999";
1196
1197 // Add to document body
1198 document.body.appendChild(floatingMsg);
1199
1200 // Animate in
1201 floatingMsg.style.opacity = "0";
1202 floatingMsg.style.transform = "translateY(10px)";
1203
1204 setTimeout(() => {
1205 floatingMsg.style.opacity = "1";
1206 floatingMsg.style.transform = "translateY(0)";
1207 }, 10);
1208
1209 // Remove after animation
1210 setTimeout(() => {
1211 floatingMsg.style.opacity = "0";
1212 floatingMsg.style.transform = "translateY(-10px)";
1213
1214 setTimeout(() => {
1215 document.body.removeChild(floatingMsg);
1216 }, 300);
1217 }, 1500);
1218 }
1219
philip.zeyliger6d3de482025-06-10 19:38:14 -07001220 // Format GitHub repository URL to org/repo format
1221 formatGitHubRepo(url) {
1222 if (!url) return null;
1223
1224 // Common GitHub URL patterns
1225 const patterns = [
1226 // HTTPS URLs
1227 /https:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
1228 // SSH URLs
1229 /git@github\.com:([^/]+)\/([^/\s.]+)(?:\.git)?/,
1230 // Git protocol
1231 /git:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
1232 ];
1233
1234 for (const pattern of patterns) {
1235 const match = url.match(pattern);
1236 if (match) {
1237 return {
1238 formatted: `${match[1]}/${match[2]}`,
1239 url: `https://github.com/${match[1]}/${match[2]}`,
1240 owner: match[1],
1241 repo: match[2],
1242 };
1243 }
1244 }
1245
1246 return null;
1247 }
1248
1249 // Generate GitHub branch URL if linking is enabled
1250 getGitHubBranchLink(branchName) {
1251 if (!this.state?.link_to_github || !branchName) {
1252 return null;
1253 }
1254
1255 const github = this.formatGitHubRepo(this.state?.git_origin);
1256 if (!github) {
1257 return null;
1258 }
1259
1260 return `https://github.com/${github.owner}/${github.repo}/tree/${branchName}`;
1261 }
1262
Sean McCullough86b56862025-04-18 13:04:03 -07001263 render() {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001264 // Calculate if this is an end of turn message with no parent conversation ID
1265 const isEndOfTurn =
1266 this.message?.end_of_turn && !this.message?.parent_conversation_id;
1267
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001268 const isPreCompaction =
1269 this.message?.idx !== undefined &&
1270 this.message.idx < this.firstMessageIndex;
1271
Sean McCullough86b56862025-04-18 13:04:03 -07001272 return html`
1273 <div
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001274 class="message ${this.message?.type} ${isEndOfTurn
Sean McCullough86b56862025-04-18 13:04:03 -07001275 ? "end-of-turn"
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001276 : ""} ${isPreCompaction ? "pre-compaction" : ""}"
Sean McCullough86b56862025-04-18 13:04:03 -07001277 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001278 <div class="message-container">
1279 <!-- Left area (empty for simplicity) -->
1280 <div class="message-metadata-left"></div>
1281
1282 <!-- Message bubble -->
1283 <div class="message-bubble-container">
1284 <div class="message-content">
1285 <div class="message-text-container">
1286 <div class="message-actions">
1287 ${copyButton(this.message?.content)}
1288 <button
1289 class="info-icon"
1290 title="Show message details"
1291 @click=${this._toggleInfo}
Sean McCullough71941bd2025-04-18 13:31:48 -07001292 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001293 <svg
1294 xmlns="http://www.w3.org/2000/svg"
1295 width="16"
1296 height="16"
1297 viewBox="0 0 24 24"
1298 fill="none"
1299 stroke="currentColor"
1300 stroke-width="2"
1301 stroke-linecap="round"
1302 stroke-linejoin="round"
1303 >
1304 <circle cx="12" cy="12" r="10"></circle>
1305 <line x1="12" y1="16" x2="12" y2="12"></line>
1306 <line x1="12" y1="8" x2="12.01" y2="8"></line>
1307 </svg>
1308 </button>
1309 </div>
1310 ${this.message?.content
1311 ? html`
1312 <div class="message-text markdown-content">
1313 ${unsafeHTML(
1314 this.renderMarkdown(this.message?.content),
1315 )}
1316 </div>
1317 `
1318 : ""}
1319
1320 <!-- End of turn indicator inside the bubble -->
1321 ${isEndOfTurn && this.message?.elapsed
1322 ? html`
1323 <div class="end-of-turn-indicator">
1324 end of turn
1325 (${this._formatDuration(this.message?.elapsed)})
1326 </div>
1327 `
1328 : ""}
1329
1330 <!-- Info panel that can be toggled -->
1331 ${this.showInfo
1332 ? html`
1333 <div class="message-info-panel">
1334 <div class="info-row">
1335 <span class="info-label">Type:</span>
1336 <span class="info-value">${this.message?.type}</span>
1337 </div>
1338 <div class="info-row">
1339 <span class="info-label">Time:</span>
1340 <span class="info-value"
1341 >${this.formatTimestamp(
1342 this.message?.timestamp,
1343 "",
1344 )}</span
1345 >
1346 </div>
1347 ${this.message?.elapsed
1348 ? html`
1349 <div class="info-row">
1350 <span class="info-label">Duration:</span>
1351 <span class="info-value"
1352 >${this._formatDuration(
1353 this.message?.elapsed,
1354 )}</span
1355 >
1356 </div>
1357 `
1358 : ""}
1359 ${this.message?.usage
1360 ? html`
1361 <div class="info-row">
1362 <span class="info-label">Tokens:</span>
1363 <span class="info-value">
1364 ${this.message?.usage
1365 ? html`
1366 <div>
1367 Input:
1368 ${this.formatNumber(
1369 this.message?.usage?.input_tokens ||
1370 0,
1371 )}
1372 </div>
1373 ${this.message?.usage
1374 ?.cache_creation_input_tokens
1375 ? html`
1376 <div>
1377 Cache creation:
1378 ${this.formatNumber(
1379 this.message?.usage
1380 ?.cache_creation_input_tokens,
1381 )}
1382 </div>
1383 `
1384 : ""}
1385 ${this.message?.usage
1386 ?.cache_read_input_tokens
1387 ? html`
1388 <div>
1389 Cache read:
1390 ${this.formatNumber(
1391 this.message?.usage
1392 ?.cache_read_input_tokens,
1393 )}
1394 </div>
1395 `
1396 : ""}
1397 <div>
1398 Output:
1399 ${this.formatNumber(
1400 this.message?.usage?.output_tokens,
1401 )}
1402 </div>
1403 <div>
1404 Cost:
1405 ${this.formatCurrency(
1406 this.message?.usage?.cost_usd,
1407 )}
1408 </div>
1409 `
1410 : "N/A"}
1411 </span>
1412 </div>
1413 `
1414 : ""}
1415 ${this.message?.conversation_id
1416 ? html`
1417 <div class="info-row">
1418 <span class="info-label">Conversation ID:</span>
1419 <span class="info-value conversation-id"
1420 >${this.message?.conversation_id}</span
1421 >
1422 </div>
1423 `
1424 : ""}
1425 </div>
1426 `
1427 : ""}
1428 </div>
1429
1430 <!-- Tool calls - only shown for agent messages -->
1431 ${this.message?.type === "agent"
1432 ? html`
1433 <sketch-tool-calls
1434 .toolCalls=${this.message?.tool_calls}
1435 .open=${this.open}
1436 ></sketch-tool-calls>
1437 `
1438 : ""}
1439
1440 <!-- Commits section (redesigned as bubbles) -->
1441 ${this.message?.commits
1442 ? html`
1443 <div class="commits-container">
1444 <div class="commit-notification">
1445 ${this.message.commits.length} new
1446 commit${this.message.commits.length > 1 ? "s" : ""}
1447 detected
1448 </div>
1449 ${this.message.commits.map((commit) => {
1450 return html`
1451 <div class="commit-card">
Philip Zeyliger72682df2025-04-23 13:09:46 -07001452 <span
1453 class="commit-hash"
1454 title="Click to copy: ${commit.hash}"
1455 @click=${(e) =>
1456 this.copyToClipboard(
1457 commit.hash.substring(0, 8),
1458 e,
1459 )}
1460 >
Pokey Rule7be879f2025-04-23 15:30:15 +01001461 ${commit.hash.substring(0, 8)}
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001462 </span>
1463 ${commit.pushed_branch
philip.zeyliger6d3de482025-06-10 19:38:14 -07001464 ? (() => {
1465 const githubLink = this.getGitHubBranchLink(
1466 commit.pushed_branch,
1467 );
1468 return html`
1469 <div class="commit-branch-container">
1470 <span
1471 class="commit-branch pushed-branch"
1472 title="Click to copy: ${commit.pushed_branch}"
1473 @click=${(e) =>
1474 this.copyToClipboard(
1475 commit.pushed_branch,
1476 e,
1477 )}
1478 >${commit.pushed_branch}</span
1479 >
cbroebbdee42025-06-20 09:57:44 +00001480 <span
1481 class="copy-icon"
1482 @click=${(e) => {
1483 e.stopPropagation();
1484 this.copyToClipboard(
1485 commit.pushed_branch,
1486 e,
1487 );
1488 }}
1489 >
philip.zeyliger6d3de482025-06-10 19:38:14 -07001490 <svg
1491 xmlns="http://www.w3.org/2000/svg"
1492 width="14"
1493 height="14"
1494 viewBox="0 0 24 24"
1495 fill="none"
1496 stroke="currentColor"
1497 stroke-width="2"
1498 stroke-linecap="round"
1499 stroke-linejoin="round"
1500 >
1501 <rect
1502 x="9"
1503 y="9"
1504 width="13"
1505 height="13"
1506 rx="2"
1507 ry="2"
1508 ></rect>
1509 <path
1510 d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
1511 ></path>
1512 </svg>
1513 </span>
1514 ${githubLink
1515 ? html`
1516 <a
1517 href="${githubLink}"
1518 target="_blank"
1519 rel="noopener noreferrer"
1520 class="octocat-link"
1521 title="Open ${commit.pushed_branch} on GitHub"
1522 @click=${(e) =>
1523 e.stopPropagation()}
1524 >
1525 <svg
1526 class="octocat-icon"
1527 viewBox="0 0 16 16"
1528 width="14"
1529 height="14"
1530 >
1531 <path
1532 fill="currentColor"
1533 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"
1534 />
1535 </svg>
1536 </a>
1537 `
1538 : ""}
1539 </div>
1540 `;
1541 })()
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001542 : ``}
1543 <span class="commit-subject"
1544 >${commit.subject}</span
Sean McCullough71941bd2025-04-18 13:31:48 -07001545 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001546 <button
1547 class="commit-diff-button"
1548 @click=${() => this.showCommit(commit.hash)}
1549 >
1550 View Diff
1551 </button>
Sean McCullough86b56862025-04-18 13:04:03 -07001552 </div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001553 `;
1554 })}
1555 </div>
1556 `
1557 : ""}
1558 </div>
1559 </div>
1560
1561 <!-- Right side (empty for consistency) -->
1562 <div class="message-metadata-right"></div>
Sean McCullough86b56862025-04-18 13:04:03 -07001563 </div>
1564 </div>
1565 `;
1566 }
1567}
1568
Sean McCullough71941bd2025-04-18 13:31:48 -07001569function copyButton(textToCopy: string) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001570 // Use an icon of overlapping rectangles for copy
1571 const buttonClass = "copy-icon";
1572
1573 // SVG for copy icon (two overlapping rectangles)
1574 const copyIcon = html`<svg
1575 xmlns="http://www.w3.org/2000/svg"
1576 width="16"
1577 height="16"
1578 viewBox="0 0 24 24"
1579 fill="none"
1580 stroke="currentColor"
1581 stroke-width="2"
1582 stroke-linecap="round"
1583 stroke-linejoin="round"
1584 >
1585 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1586 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1587 </svg>`;
1588
1589 // SVG for success check mark
1590 const successIcon = html`<svg
1591 xmlns="http://www.w3.org/2000/svg"
1592 width="16"
1593 height="16"
1594 viewBox="0 0 24 24"
1595 fill="none"
1596 stroke="currentColor"
1597 stroke-width="2"
1598 stroke-linecap="round"
1599 stroke-linejoin="round"
1600 >
1601 <path d="M20 6L9 17l-5-5"></path>
1602 </svg>`;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001603
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001604 const ret = html`<button
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001605 class="${buttonClass}"
1606 title="Copy to clipboard"
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001607 @click=${(e: Event) => {
1608 e.stopPropagation();
1609 const copyButton = e.currentTarget as HTMLButtonElement;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001610 const originalInnerHTML = copyButton.innerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001611 navigator.clipboard
1612 .writeText(textToCopy)
1613 .then(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001614 copyButton.innerHTML = "";
1615 const successElement = document.createElement("div");
1616 copyButton.appendChild(successElement);
1617 render(successIcon, successElement);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001618 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001619 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001620 }, 2000);
1621 })
1622 .catch((err) => {
1623 console.error("Failed to copy text: ", err);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001624 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001625 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001626 }, 2000);
1627 });
1628 }}
1629 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001630 ${copyIcon}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001631 </button>`;
Sean McCullough86b56862025-04-18 13:04:03 -07001632
Sean McCullough71941bd2025-04-18 13:31:48 -07001633 return ret;
Sean McCullough86b56862025-04-18 13:04:03 -07001634}
1635
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001636// Create global styles for floating messages
1637const floatingMessageStyles = document.createElement("style");
1638floatingMessageStyles.textContent = `
1639 .floating-message {
1640 background-color: rgba(0, 0, 0, 0.8);
1641 color: white;
1642 padding: 5px 10px;
1643 border-radius: 4px;
1644 font-size: 12px;
1645 font-family: system-ui, sans-serif;
1646 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
1647 pointer-events: none;
1648 transition: opacity 0.3s ease, transform 0.3s ease;
1649 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001650
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001651 .floating-message.success {
1652 background-color: rgba(40, 167, 69, 0.9);
1653 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001654
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001655 .floating-message.error {
1656 background-color: rgba(220, 53, 69, 0.9);
1657 }
Philip Zeyligere31d2a92025-05-11 15:22:35 -07001658
1659 /* Style for code, pre elements, and tool components to ensure proper wrapping/truncation */
1660 pre, code, sketch-tool-calls, sketch-tool-card, sketch-tool-card-bash {
1661 white-space: nowrap;
1662 overflow: hidden;
1663 text-overflow: ellipsis;
1664 max-width: 100%;
1665 }
1666
1667 /* Special rule for the message content container */
1668 .message-content {
1669 max-width: 100% !important;
1670 overflow: hidden !important;
1671 }
1672
1673 /* Ensure tool call containers don't overflow */
1674 ::slotted(sketch-tool-calls) {
1675 max-width: 100%;
1676 width: 100%;
1677 overflow-wrap: break-word;
1678 word-break: break-word;
1679 }
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001680`;
1681document.head.appendChild(floatingMessageStyles);
1682
Sean McCullough86b56862025-04-18 13:04:03 -07001683declare global {
1684 interface HTMLElementTagNameMap {
1685 "sketch-timeline-message": SketchTimelineMessage;
1686 }
1687}