blob: bad9e4398cf60198da42220adefe0e273fd996ab [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
David Crawshaw4b644682025-06-26 17:15:10 +000075 @property({ type: Boolean, reflect: true, attribute: "compactpadding" })
76 compactPadding: boolean = false;
77
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000078 @state()
79 showInfo: boolean = false;
80
Sean McCullough86b56862025-04-18 13:04:03 -070081 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
82 // Note that these styles only apply to the scope of this web component's
83 // shadow DOM node, so they won't leak out or collide with CSS declared in
84 // other components or the containing web page (...unless you want it to do that).
85 static styles = css`
86 .message {
87 position: relative;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000088 margin-bottom: 6px;
89 display: flex;
90 flex-direction: column;
91 width: 100%;
Sean McCullough86b56862025-04-18 13:04:03 -070092 }
93
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000094 .message-container {
95 display: flex;
96 position: relative;
97 width: 100%;
98 }
99
100 .message-metadata-left {
101 flex: 0 0 80px;
102 padding: 3px 5px;
103 text-align: right;
104 font-size: 11px;
105 color: #777;
106 align-self: flex-start;
107 }
108
109 .message-metadata-right {
110 flex: 0 0 80px;
111 padding: 3px 5px;
112 text-align: left;
113 font-size: 11px;
114 color: #777;
115 align-self: flex-start;
116 }
117
118 .message-bubble-container {
119 flex: 1;
120 display: flex;
121 max-width: calc(100% - 160px);
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700122 overflow: hidden;
123 text-overflow: ellipsis;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000124 }
125
David Crawshaw4b644682025-06-26 17:15:10 +0000126 :host([compactpadding]) .message-bubble-container {
127 max-width: 100%;
128 }
129
130 :host([compactpadding]) .message-metadata-left,
131 :host([compactpadding]) .message-metadata-right {
132 display: none;
133 }
134
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000135 .user .message-bubble-container {
136 justify-content: flex-end;
137 }
138
139 .agent .message-bubble-container,
140 .tool .message-bubble-container,
141 .error .message-bubble-container {
142 justify-content: flex-start;
Sean McCullough86b56862025-04-18 13:04:03 -0700143 }
144
145 .message-content {
146 position: relative;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000147 padding: 6px 10px;
148 border-radius: 12px;
149 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700150 max-width: 100%;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000151 width: fit-content;
152 min-width: min-content;
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700153 overflow-wrap: break-word;
154 word-break: break-word;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000155 }
156
157 /* User message styling */
158 .user .message-content {
159 background-color: #2196f3;
160 color: white;
161 border-bottom-right-radius: 5px;
162 }
163
164 /* Agent message styling */
165 .agent .message-content,
166 .tool .message-content,
167 .error .message-content {
168 background-color: #f1f1f1;
169 color: black;
170 border-bottom-left-radius: 5px;
Sean McCullough86b56862025-04-18 13:04:03 -0700171 }
172
173 /* Copy button styles */
174 .message-text-container,
175 .tool-result-container {
176 position: relative;
177 }
178
179 .message-actions {
180 position: absolute;
181 top: 5px;
182 right: 5px;
183 z-index: 10;
184 opacity: 0;
185 transition: opacity 0.2s ease;
186 }
187
188 .message-text-container:hover .message-actions,
189 .tool-result-container:hover .message-actions {
190 opacity: 1;
191 }
192
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000193 .message-actions {
Sean McCullough86b56862025-04-18 13:04:03 -0700194 display: flex;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000195 gap: 6px;
196 }
197
198 .copy-icon,
199 .info-icon {
200 background-color: transparent;
201 border: none;
202 color: rgba(0, 0, 0, 0.6);
203 cursor: pointer;
204 padding: 3px;
205 border-radius: 50%;
206 display: flex;
207 align-items: center;
208 justify-content: center;
209 width: 24px;
210 height: 24px;
211 transition: all 0.15s ease;
212 }
213
214 .user .copy-icon,
215 .user .info-icon {
216 color: rgba(255, 255, 255, 0.8);
217 }
218
219 .copy-icon:hover,
220 .info-icon:hover {
221 background-color: rgba(0, 0, 0, 0.08);
222 }
223
224 .user .copy-icon:hover,
225 .user .info-icon:hover {
226 background-color: rgba(255, 255, 255, 0.15);
227 }
228
229 /* Message metadata styling */
230 .message-type {
231 font-weight: bold;
232 font-size: 11px;
Sean McCullough86b56862025-04-18 13:04:03 -0700233 }
234
235 .message-timestamp {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000236 display: block;
Sean McCullough86b56862025-04-18 13:04:03 -0700237 font-size: 10px;
238 color: #888;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000239 margin-top: 2px;
240 }
241
242 .message-duration {
243 display: block;
244 font-size: 10px;
245 color: #888;
246 margin-top: 2px;
Sean McCullough86b56862025-04-18 13:04:03 -0700247 }
248
249 .message-usage {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000250 display: block;
Sean McCullough86b56862025-04-18 13:04:03 -0700251 font-size: 10px;
252 color: #888;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000253 margin-top: 3px;
Sean McCullough86b56862025-04-18 13:04:03 -0700254 }
255
256 .conversation-id {
257 font-family: monospace;
258 font-size: 12px;
259 padding: 2px 4px;
Sean McCullough86b56862025-04-18 13:04:03 -0700260 margin-left: auto;
261 }
262
263 .parent-info {
264 font-size: 11px;
265 opacity: 0.8;
266 }
267
268 .subconversation {
269 border-left: 2px solid transparent;
270 padding-left: 5px;
271 margin-left: 20px;
272 transition: margin-left 0.3s ease;
273 }
274
275 .message-text {
276 overflow-x: auto;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000277 margin-bottom: 0;
278 font-family: sans-serif;
279 padding: 2px 0;
Sean McCullough86b56862025-04-18 13:04:03 -0700280 user-select: text;
281 cursor: text;
282 -webkit-user-select: text;
283 -moz-user-select: text;
284 -ms-user-select: text;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000285 font-size: 14px;
286 line-height: 1.35;
287 text-align: left;
288 }
289
290 /* Style for code blocks within messages */
291 .message-text pre,
292 .message-text code {
293 font-family: monospace;
294 background: rgba(0, 0, 0, 0.05);
295 border-radius: 4px;
296 padding: 2px 4px;
297 overflow-x: auto;
298 max-width: 100%;
299 white-space: pre-wrap; /* Allow wrapping for very long lines */
300 word-break: break-all; /* Break words at any character */
301 box-sizing: border-box; /* Include padding in width calculation */
302 }
303
Pokey Rulea10f1512025-05-15 13:53:26 +0000304 /* Code block container styles */
305 .code-block-container {
306 position: relative;
307 margin: 8px 0;
308 border-radius: 6px;
309 overflow: hidden;
310 background: rgba(0, 0, 0, 0.05);
311 }
312
313 .user .code-block-container {
314 background: rgba(255, 255, 255, 0.2);
315 }
316
317 .code-block-header {
318 display: flex;
319 justify-content: space-between;
320 align-items: center;
321 padding: 4px 8px;
322 background: rgba(0, 0, 0, 0.1);
323 font-size: 12px;
324 }
325
326 .user .code-block-header {
327 background: rgba(255, 255, 255, 0.2);
328 color: white;
329 }
330
331 .code-language {
332 font-family: monospace;
333 font-size: 11px;
334 font-weight: 500;
335 }
336
337 .code-copy-button {
338 background: transparent;
339 border: none;
340 color: inherit;
341 cursor: pointer;
342 padding: 2px;
343 border-radius: 3px;
344 display: flex;
345 align-items: center;
346 justify-content: center;
347 opacity: 0.7;
348 transition: all 0.15s ease;
349 }
350
351 .code-copy-button:hover {
352 opacity: 1;
353 background: rgba(0, 0, 0, 0.1);
354 }
355
356 .user .code-copy-button:hover {
357 background: rgba(255, 255, 255, 0.2);
358 }
359
360 .code-block-container pre {
361 margin: 0;
362 padding: 8px;
363 background: transparent;
364 }
365
366 .code-block-container code {
367 background: transparent;
368 padding: 0;
369 display: block;
370 width: 100%;
371 }
372
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000373 .user .message-text pre,
374 .user .message-text code {
375 background: rgba(255, 255, 255, 0.2);
376 color: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700377 }
378
379 .tool-details {
380 margin-top: 3px;
381 padding-top: 3px;
382 border-top: 1px dashed #e0e0e0;
383 font-size: 12px;
384 }
385
386 .tool-name {
387 font-size: 12px;
388 font-weight: bold;
389 margin-bottom: 2px;
390 background: #f0f0f0;
391 padding: 2px 4px;
392 border-radius: 2px;
393 display: flex;
394 align-items: center;
395 gap: 3px;
396 }
397
398 .tool-input,
399 .tool-result {
400 margin-top: 2px;
401 padding: 3px 5px;
402 background: #f7f7f7;
403 border-radius: 2px;
404 font-family: monospace;
405 font-size: 12px;
406 overflow-x: auto;
407 white-space: pre;
408 line-height: 1.3;
409 user-select: text;
410 cursor: text;
411 -webkit-user-select: text;
412 -moz-user-select: text;
413 -ms-user-select: text;
414 }
415
416 .tool-result {
417 max-height: 300px;
418 overflow-y: auto;
419 }
420
421 .usage-info {
422 margin-top: 10px;
423 padding-top: 10px;
424 border-top: 1px dashed #e0e0e0;
425 font-size: 12px;
426 color: #666;
427 }
428
429 /* Custom styles for IRC-like experience */
430 .user .message-content {
431 border-left-color: #2196f3;
432 }
433
434 .agent .message-content {
435 border-left-color: #4caf50;
436 }
437
438 .tool .message-content {
439 border-left-color: #ff9800;
440 }
441
442 .error .message-content {
443 border-left-color: #f44336;
444 }
445
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700446 /* Compact message styling - distinct visual separation */
447 .compact {
448 background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
449 border: 2px solid #fd7e14;
450 border-radius: 12px;
451 margin: 20px 0;
452 padding: 0;
453 }
454
455 .compact .message-content {
456 border-left: 4px solid #fd7e14;
457 background: rgba(253, 126, 20, 0.05);
458 font-weight: 500;
459 }
460
461 .compact .message-text {
462 color: #8b4513;
463 font-size: 13px;
464 line-height: 1.4;
465 }
466
467 .compact::before {
468 content: "📚 CONVERSATION EPOCH";
469 display: block;
470 text-align: center;
471 font-size: 11px;
472 font-weight: bold;
473 color: #8b4513;
474 background: #fd7e14;
475 color: white;
476 padding: 4px 8px;
477 margin: 0;
478 border-radius: 8px 8px 0 0;
479 letter-spacing: 1px;
480 }
481
482 /* Pre-compaction messages get a subtle diagonal stripe background */
483 .pre-compaction {
484 background: repeating-linear-gradient(
485 45deg,
486 #ffffff,
487 #ffffff 10px,
488 #f8f8f8 10px,
489 #f8f8f8 20px
490 );
491 opacity: 0.85;
492 border-left: 3px solid #ddd;
493 }
494
495 .pre-compaction .message-content {
496 background: rgba(255, 255, 255, 0.7);
497 backdrop-filter: blur(1px);
Philip Zeyliger57d28bc2025-06-06 20:28:34 +0000498 color: #333; /* Ensure dark text for readability */
499 }
500
501 .pre-compaction .message-text {
502 color: #333; /* Ensure dark text in message content */
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700503 }
504
Sean McCullough86b56862025-04-18 13:04:03 -0700505 /* Make message type display bold but without the IRC-style markers */
506 .message-type {
507 font-weight: bold;
508 }
509
510 /* Commit message styling */
Sean McCullough86b56862025-04-18 13:04:03 -0700511 .commits-container {
512 margin-top: 10px;
Sean McCullough86b56862025-04-18 13:04:03 -0700513 }
514
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000515 .commit-notification {
516 background-color: #e8f5e9;
517 color: #2e7d32;
518 font-weight: 500;
519 font-size: 12px;
520 padding: 6px 10px;
521 border-radius: 10px;
522 margin-bottom: 8px;
523 text-align: center;
524 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
Sean McCullough86b56862025-04-18 13:04:03 -0700525 }
526
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000527 .commit-card {
528 background-color: #f5f5f5;
529 border-radius: 8px;
Sean McCullough86b56862025-04-18 13:04:03 -0700530 overflow: hidden;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000531 margin-bottom: 6px;
532 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
533 padding: 6px 8px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000534 display: flex;
535 align-items: center;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000536 gap: 8px;
Sean McCullough86b56862025-04-18 13:04:03 -0700537 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700538
Sean McCullough86b56862025-04-18 13:04:03 -0700539 .commit-hash {
540 color: #0366d6;
541 font-weight: bold;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000542 font-family: monospace;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000543 cursor: pointer;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000544 text-decoration: none;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000545 background-color: rgba(3, 102, 214, 0.08);
546 padding: 2px 5px;
547 border-radius: 4px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000548 }
549
550 .commit-hash:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000551 background-color: rgba(3, 102, 214, 0.15);
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000552 }
553
554 .commit-branch {
555 color: #28a745;
556 font-weight: 500;
557 cursor: pointer;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000558 font-family: monospace;
559 background-color: rgba(40, 167, 69, 0.08);
560 padding: 2px 5px;
561 border-radius: 4px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000562 }
563
564 .commit-branch:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000565 background-color: rgba(40, 167, 69, 0.15);
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000566 }
567
philip.zeyliger6d3de482025-06-10 19:38:14 -0700568 .commit-branch-container {
569 display: flex;
570 align-items: center;
571 gap: 6px;
572 }
573
574 .commit-branch-container .copy-icon {
575 opacity: 0.7;
576 display: flex;
577 align-items: center;
578 }
579
580 .commit-branch-container .copy-icon svg {
581 vertical-align: middle;
582 }
583
584 .commit-branch-container:hover .copy-icon {
585 opacity: 1;
586 }
587
588 .octocat-link {
589 color: #586069;
590 text-decoration: none;
591 display: flex;
592 align-items: center;
593 transition: color 0.2s ease;
594 }
595
596 .octocat-link:hover {
597 color: #0366d6;
598 }
599
600 .octocat-icon {
601 width: 14px;
602 height: 14px;
603 }
604
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000605 .commit-subject {
606 font-size: 13px;
607 color: #333;
608 flex-grow: 1;
609 overflow: hidden;
610 text-overflow: ellipsis;
611 white-space: nowrap;
Sean McCullough86b56862025-04-18 13:04:03 -0700612 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700613
Sean McCullough86b56862025-04-18 13:04:03 -0700614 .commit-diff-button {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000615 padding: 3px 8px;
616 border: none;
617 border-radius: 4px;
618 background-color: #0366d6;
619 color: white;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000620 font-size: 11px;
Sean McCullough86b56862025-04-18 13:04:03 -0700621 cursor: pointer;
622 transition: all 0.2s ease;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000623 display: block;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000624 margin-left: auto;
Sean McCullough86b56862025-04-18 13:04:03 -0700625 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700626
Sean McCullough86b56862025-04-18 13:04:03 -0700627 .commit-diff-button:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000628 background-color: #0256b4;
Sean McCullough86b56862025-04-18 13:04:03 -0700629 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700630
Sean McCullough86b56862025-04-18 13:04:03 -0700631 /* Tool call cards */
632 .tool-call-cards-container {
633 display: flex;
634 flex-direction: column;
635 gap: 8px;
636 margin-top: 8px;
637 }
638
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000639 /* Error message specific styling */
640 .error .message-content {
641 background-color: #ffebee;
642 border-left: 3px solid #f44336;
Sean McCullough86b56862025-04-18 13:04:03 -0700643 }
644
645 .end-of-turn {
646 margin-bottom: 15px;
647 }
648
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000649 .end-of-turn-indicator {
650 display: block;
651 font-size: 11px;
652 color: #777;
653 padding: 2px 0;
654 margin-top: 8px;
655 text-align: right;
656 font-style: italic;
657 }
658
659 .user .end-of-turn-indicator {
660 color: rgba(255, 255, 255, 0.7);
661 }
662
663 /* Message info panel styling */
664 .message-info-panel {
665 margin-top: 8px;
666 padding: 8px;
667 background-color: rgba(0, 0, 0, 0.03);
668 border-radius: 6px;
669 font-size: 12px;
670 transition: all 0.2s ease;
671 border-left: 2px solid rgba(0, 0, 0, 0.1);
672 }
673
674 .user .message-info-panel {
675 background-color: rgba(255, 255, 255, 0.15);
676 border-left: 2px solid rgba(255, 255, 255, 0.2);
677 }
678
679 .info-row {
680 margin-bottom: 3px;
681 display: flex;
682 }
683
684 .info-label {
685 font-weight: bold;
686 margin-right: 5px;
687 min-width: 60px;
688 }
689
690 .info-value {
691 flex: 1;
692 }
693
694 .conversation-id {
695 font-family: monospace;
Sean McCullough86b56862025-04-18 13:04:03 -0700696 font-size: 10px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000697 word-break: break-all;
Sean McCullough86b56862025-04-18 13:04:03 -0700698 }
699
700 .markdown-content {
701 box-sizing: border-box;
702 min-width: 200px;
703 margin: 0 auto;
704 }
705
706 .markdown-content p {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000707 margin-block-start: 0.3em;
708 margin-block-end: 0.3em;
709 }
710
711 .markdown-content p:first-child {
712 margin-block-start: 0;
713 }
714
715 .markdown-content p:last-child {
716 margin-block-end: 0;
717 }
718
719 /* Styling for markdown elements */
720 .markdown-content a {
721 color: inherit;
722 text-decoration: underline;
723 }
724
725 .user .markdown-content a {
726 color: #fff;
727 text-decoration: underline;
728 }
729
730 .markdown-content ul,
731 .markdown-content ol {
732 padding-left: 1.5em;
733 margin: 0.5em 0;
734 }
735
736 .markdown-content blockquote {
737 border-left: 3px solid rgba(0, 0, 0, 0.2);
738 padding-left: 1em;
739 margin-left: 0.5em;
740 font-style: italic;
741 }
742
743 .user .markdown-content blockquote {
744 border-left: 3px solid rgba(255, 255, 255, 0.4);
Sean McCullough86b56862025-04-18 13:04:03 -0700745 }
Autoformatterdded2d62025-04-28 00:27:21 +0000746
Sean McCullough8d93e362025-04-27 23:32:18 +0000747 /* Mermaid diagram styling */
748 .mermaid-container {
749 margin: 1em 0;
750 padding: 0.5em;
751 background-color: #f8f8f8;
752 border-radius: 4px;
753 overflow-x: auto;
754 }
Autoformatterdded2d62025-04-28 00:27:21 +0000755
Sean McCullough8d93e362025-04-27 23:32:18 +0000756 .mermaid {
757 text-align: center;
758 }
philip.zeyligerffa94c62025-06-19 18:43:37 -0700759
760 /* Print styles for message components */
761 @media print {
762 .message {
763 page-break-inside: avoid;
764 margin-bottom: 12px;
765 }
766
767 .message-container {
768 page-break-inside: avoid;
769 }
770
771 /* Hide copy buttons and interactive elements during printing */
772 .copy-icon,
773 .info-icon,
774 .commit-diff-button {
775 display: none !important;
776 }
777
778 /* Ensure code blocks print properly */
779 .message-content pre {
780 white-space: pre-wrap;
781 word-wrap: break-word;
782 page-break-inside: avoid;
783 background: #f8f8f8 !important;
784 border: 1px solid #ddd !important;
785 padding: 8px !important;
786 }
787
788 /* Ensure tool calls section prints properly */
789 .tool-calls-section {
790 page-break-inside: avoid;
791 }
792
793 /* Simplify message metadata for print */
794 .message-metadata-left {
795 font-size: 10px;
796 }
797
798 /* Ensure content doesn't break poorly */
799 .message-content {
800 orphans: 3;
801 widows: 3;
802 }
803
804 /* Hide floating messages during print */
805 .floating-message {
806 display: none !important;
807 }
808 }
Sean McCullough86b56862025-04-18 13:04:03 -0700809 `;
810
Sean McCullough8d93e362025-04-27 23:32:18 +0000811 // Track mermaid diagrams that need rendering
812 private mermaidDiagrams = new Map();
813
Sean McCullough86b56862025-04-18 13:04:03 -0700814 constructor() {
815 super();
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000816 // Mermaid will be initialized lazily when first needed
Sean McCullough86b56862025-04-18 13:04:03 -0700817 }
818
819 // See https://lit.dev/docs/components/lifecycle/
820 connectedCallback() {
821 super.connectedCallback();
822 }
Autoformatterdded2d62025-04-28 00:27:21 +0000823
Sean McCullough8d93e362025-04-27 23:32:18 +0000824 // After the component is updated and rendered, render any mermaid diagrams
825 updated(changedProperties: Map<string, unknown>) {
826 super.updated(changedProperties);
827 this.renderMermaidDiagrams();
Pokey Rulea10f1512025-05-15 13:53:26 +0000828 this.setupCodeBlockCopyButtons();
Sean McCullough8d93e362025-04-27 23:32:18 +0000829 }
Autoformatterdded2d62025-04-28 00:27:21 +0000830
Sean McCullough8d93e362025-04-27 23:32:18 +0000831 // Render mermaid diagrams after the component is updated
832 renderMermaidDiagrams() {
833 // Add a small delay to ensure the DOM is fully rendered
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000834 setTimeout(async () => {
Sean McCullough8d93e362025-04-27 23:32:18 +0000835 // Find all mermaid containers in our shadow root
Autoformatterdded2d62025-04-28 00:27:21 +0000836 const containers = this.shadowRoot?.querySelectorAll(".mermaid");
Sean McCullough8d93e362025-04-27 23:32:18 +0000837 if (!containers || containers.length === 0) return;
Autoformatterdded2d62025-04-28 00:27:21 +0000838
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000839 try {
840 // Load mermaid dynamically
841 const mermaidLib = await loadMermaid();
Autoformatterdded2d62025-04-28 00:27:21 +0000842
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000843 // Initialize mermaid with specific config (only once per load)
844 mermaidLib.initialize({
845 startOnLoad: false,
846 suppressErrorRendering: true,
847 theme: "default",
848 securityLevel: "loose", // Allows more flexibility but be careful with user-generated content
849 fontFamily: "monospace",
850 });
Autoformatterdded2d62025-04-28 00:27:21 +0000851
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000852 // Process each mermaid diagram
853 containers.forEach((container) => {
854 const id = container.id;
855 const code = container.textContent || "";
856 if (!code || !id) return; // Use return for forEach instead of continue
857
858 try {
859 // Clear any previous content
860 container.innerHTML = code;
861
862 // Render the mermaid diagram using promise
863 mermaidLib
864 .render(`${id}-svg`, code)
865 .then(({ svg }) => {
866 container.innerHTML = svg;
867 })
868 .catch((err) => {
869 console.error("Error rendering mermaid diagram:", err);
870 // Show the original code as fallback
871 container.innerHTML = `<pre>${code}</pre>`;
872 });
873 } catch (err) {
874 console.error("Error processing mermaid diagram:", err);
875 // Show the original code as fallback
876 container.innerHTML = `<pre>${code}</pre>`;
877 }
878 });
879 } catch (err) {
880 console.error("Error loading mermaid:", err);
881 // Show the original code as fallback for all diagrams
882 containers.forEach((container) => {
883 const code = container.textContent || "";
Sean McCullough8d93e362025-04-27 23:32:18 +0000884 container.innerHTML = `<pre>${code}</pre>`;
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000885 });
886 }
Sean McCullough8d93e362025-04-27 23:32:18 +0000887 }, 100); // Small delay to ensure DOM is ready
888 }
Sean McCullough86b56862025-04-18 13:04:03 -0700889
Pokey Rulea10f1512025-05-15 13:53:26 +0000890 // Setup code block copy buttons after component is updated
891 setupCodeBlockCopyButtons() {
892 setTimeout(() => {
893 // Find all copy buttons in code blocks
894 const copyButtons =
895 this.shadowRoot?.querySelectorAll(".code-copy-button");
896 if (!copyButtons || copyButtons.length === 0) return;
897
898 // Add click event listener to each button
899 copyButtons.forEach((button) => {
900 button.addEventListener("click", (e) => {
901 e.stopPropagation();
902 const codeId = (button as HTMLElement).dataset.codeId;
903 if (!codeId) return;
904
905 const codeElement = this.shadowRoot?.querySelector(`#${codeId}`);
906 if (!codeElement) return;
907
908 const codeText = codeElement.textContent || "";
909 const buttonRect = button.getBoundingClientRect();
910
911 // Copy code to clipboard
912 navigator.clipboard
913 .writeText(codeText)
914 .then(() => {
915 // Show success indicator
916 const originalHTML = button.innerHTML;
917 button.innerHTML = `
918 <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">
919 <path d="M20 6L9 17l-5-5"></path>
920 </svg>
921 `;
922
923 // Display floating message
924 this.showFloatingMessage("Copied!", buttonRect, "success");
925
926 // Reset button after delay
927 setTimeout(() => {
928 button.innerHTML = originalHTML;
929 }, 2000);
930 })
931 .catch((err) => {
932 console.error("Failed to copy code:", err);
933 this.showFloatingMessage("Failed to copy!", buttonRect, "error");
934 });
935 });
936 });
937 }, 100); // Small delay to ensure DOM is ready
938 }
939
Sean McCullough86b56862025-04-18 13:04:03 -0700940 // See https://lit.dev/docs/components/lifecycle/
941 disconnectedCallback() {
942 super.disconnectedCallback();
943 }
944
945 renderMarkdown(markdownContent: string): string {
946 try {
Sean McCullough8d93e362025-04-27 23:32:18 +0000947 // Create a custom renderer
948 const renderer = new Renderer();
949 const originalCodeRenderer = renderer.code.bind(renderer);
Autoformatterdded2d62025-04-28 00:27:21 +0000950
Pokey Rulea10f1512025-05-15 13:53:26 +0000951 // Override the code renderer to handle mermaid diagrams and add copy buttons
Autoformatterdded2d62025-04-28 00:27:21 +0000952 renderer.code = function ({ text, lang, escaped }: Tokens.Code): string {
953 if (lang === "mermaid") {
Sean McCullough8d93e362025-04-27 23:32:18 +0000954 // Generate a unique ID for this diagram
955 const id = `mermaid-diagram-${Math.random().toString(36).substring(2, 10)}`;
Autoformatterdded2d62025-04-28 00:27:21 +0000956
Sean McCullough8d93e362025-04-27 23:32:18 +0000957 // Just create the container and mermaid div - we'll render it in the updated() lifecycle method
958 return `<div class="mermaid-container">
959 <div class="mermaid" id="${id}">${text}</div>
960 </div>`;
961 }
Pokey Rulea10f1512025-05-15 13:53:26 +0000962
Philip Zeyliger0d092842025-06-09 18:57:12 -0700963 // For regular code blocks, call the original renderer to get properly escaped HTML
964 const originalCodeHtml = originalCodeRenderer({ text, lang, escaped });
965
966 // Extract the code content from the original HTML to add our custom wrapper
967 // The original renderer returns: <pre><code class="language-x">escapedText</code></pre>
968 const codeMatch = originalCodeHtml.match(
969 /<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/,
970 );
971 if (!codeMatch) {
972 // Fallback to original if we can't parse it
973 return originalCodeHtml;
974 }
975
976 const escapedText = codeMatch[1];
Pokey Rulea10f1512025-05-15 13:53:26 +0000977 const id = `code-block-${Math.random().toString(36).substring(2, 10)}`;
978 const langClass = lang ? ` class="language-${lang}"` : "";
979
980 return `<div class="code-block-container">
981 <div class="code-block-header">
982 ${lang ? `<span class="code-language">${lang}</span>` : ""}
983 <button class="code-copy-button" title="Copy code" data-code-id="${id}">
984 <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">
985 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
986 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
987 </svg>
988 </button>
989 </div>
Philip Zeyliger0d092842025-06-09 18:57:12 -0700990 <pre><code id="${id}"${langClass}>${escapedText}</code></pre>
Pokey Rulea10f1512025-05-15 13:53:26 +0000991 </div>`;
Sean McCullough8d93e362025-04-27 23:32:18 +0000992 };
Autoformatterdded2d62025-04-28 00:27:21 +0000993
Philip Zeyliger53ab2452025-06-04 17:49:33 +0000994 // Set markdown options for proper code block highlighting
Sean McCullough86b56862025-04-18 13:04:03 -0700995 const markedOptions: MarkedOptions = {
996 gfm: true, // GitHub Flavored Markdown
997 breaks: true, // Convert newlines to <br>
998 async: false,
Autoformatterdded2d62025-04-28 00:27:21 +0000999 renderer: renderer,
Sean McCullough86b56862025-04-18 13:04:03 -07001000 };
Philip Zeyliger53ab2452025-06-04 17:49:33 +00001001
1002 // Parse markdown and sanitize the output HTML with DOMPurify
1003 const htmlOutput = marked.parse(markdownContent, markedOptions) as string;
1004 return DOMPurify.sanitize(htmlOutput, {
1005 // Allow common HTML elements that are safe
1006 ALLOWED_TAGS: [
1007 "p",
1008 "br",
1009 "strong",
1010 "em",
1011 "b",
1012 "i",
1013 "u",
1014 "s",
1015 "code",
1016 "pre",
1017 "h1",
1018 "h2",
1019 "h3",
1020 "h4",
1021 "h5",
1022 "h6",
1023 "ul",
1024 "ol",
1025 "li",
1026 "blockquote",
1027 "a",
1028 "div",
1029 "span", // For mermaid diagrams and code blocks
1030 "svg",
1031 "g",
1032 "path",
1033 "rect",
1034 "circle",
1035 "text",
1036 "line",
1037 "polygon", // For mermaid SVG
1038 "button", // For code copy buttons
1039 ],
1040 ALLOWED_ATTR: [
1041 "href",
1042 "title",
1043 "target",
1044 "rel", // For links
1045 "class",
1046 "id", // For styling and functionality
1047 "data-*", // For code copy buttons
1048 // SVG attributes for mermaid diagrams
1049 "viewBox",
1050 "width",
1051 "height",
1052 "xmlns",
1053 "fill",
1054 "stroke",
1055 "stroke-width",
1056 "d",
1057 "x",
1058 "y",
1059 "x1",
1060 "y1",
1061 "x2",
1062 "y2",
1063 "cx",
1064 "cy",
1065 "r",
1066 "rx",
1067 "ry",
1068 "points",
1069 "transform",
1070 "text-anchor",
1071 "font-size",
1072 "font-family",
1073 ],
1074 // Allow data attributes for functionality
1075 ALLOW_DATA_ATTR: true,
1076 // Keep whitespace for code formatting
1077 KEEP_CONTENT: true,
1078 });
Sean McCullough86b56862025-04-18 13:04:03 -07001079 } catch (error) {
1080 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +00001081 // Fallback to sanitized plain text if markdown parsing fails
1082 return DOMPurify.sanitize(markdownContent);
Sean McCullough86b56862025-04-18 13:04:03 -07001083 }
1084 }
1085
1086 /**
1087 * Format timestamp for display
1088 */
1089 formatTimestamp(
1090 timestamp: string | number | Date | null | undefined,
1091 defaultValue: string = "",
1092 ): string {
1093 if (!timestamp) return defaultValue;
1094 try {
1095 const date = new Date(timestamp);
1096 if (isNaN(date.getTime())) return defaultValue;
1097
1098 // Format: Mar 13, 2025 09:53:25 AM
1099 return date.toLocaleString("en-US", {
1100 month: "short",
1101 day: "numeric",
1102 year: "numeric",
1103 hour: "numeric",
1104 minute: "2-digit",
1105 second: "2-digit",
1106 hour12: true,
1107 });
1108 } catch (e) {
1109 return defaultValue;
1110 }
1111 }
1112
1113 formatNumber(
1114 num: number | null | undefined,
1115 defaultValue: string = "0",
1116 ): string {
1117 if (num === undefined || num === null) return defaultValue;
1118 try {
1119 return num.toLocaleString();
1120 } catch (e) {
1121 return String(num);
1122 }
1123 }
1124 formatCurrency(
1125 num: number | string | null | undefined,
1126 defaultValue: string = "$0.00",
1127 isMessageLevel: boolean = false,
1128 ): string {
1129 if (num === undefined || num === null) return defaultValue;
1130 try {
1131 // Use 4 decimal places for message-level costs, 2 for totals
1132 const decimalPlaces = isMessageLevel ? 4 : 2;
1133 return `$${parseFloat(String(num)).toFixed(decimalPlaces)}`;
1134 } catch (e) {
1135 return defaultValue;
1136 }
1137 }
1138
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001139 // Format duration from nanoseconds to a human-readable string
1140 _formatDuration(nanoseconds: number | null | undefined): string {
1141 if (!nanoseconds) return "0s";
1142
1143 const seconds = nanoseconds / 1e9;
1144
1145 if (seconds < 60) {
1146 return `${seconds.toFixed(1)}s`;
1147 } else if (seconds < 3600) {
1148 const minutes = Math.floor(seconds / 60);
1149 const remainingSeconds = seconds % 60;
1150 return `${minutes}min ${remainingSeconds.toFixed(0)}s`;
1151 } else {
1152 const hours = Math.floor(seconds / 3600);
1153 const remainingSeconds = seconds % 3600;
1154 const minutes = Math.floor(remainingSeconds / 60);
1155 return `${hours}h ${minutes}min`;
1156 }
1157 }
1158
Sean McCullough86b56862025-04-18 13:04:03 -07001159 showCommit(commitHash: string) {
Sean McCullough71941bd2025-04-18 13:31:48 -07001160 this.dispatchEvent(
1161 new CustomEvent("show-commit-diff", {
1162 bubbles: true,
1163 composed: true,
1164 detail: { commitHash },
1165 }),
1166 );
Sean McCullough86b56862025-04-18 13:04:03 -07001167 }
1168
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001169 _toggleInfo(e: Event) {
1170 e.stopPropagation();
1171 this.showInfo = !this.showInfo;
1172 }
1173
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001174 copyToClipboard(text: string, event: Event) {
1175 const element = event.currentTarget as HTMLElement;
1176 const rect = element.getBoundingClientRect();
1177
1178 navigator.clipboard
1179 .writeText(text)
1180 .then(() => {
1181 this.showFloatingMessage("Copied!", rect, "success");
1182 })
1183 .catch((err) => {
1184 console.error("Failed to copy text: ", err);
1185 this.showFloatingMessage("Failed to copy!", rect, "error");
1186 });
1187 }
1188
1189 showFloatingMessage(
1190 message: string,
1191 targetRect: DOMRect,
1192 type: "success" | "error",
1193 ) {
1194 // Create floating message element
1195 const floatingMsg = document.createElement("div");
1196 floatingMsg.textContent = message;
1197 floatingMsg.className = `floating-message ${type}`;
1198
1199 // Position it near the clicked element
1200 // Position just above the element
1201 const top = targetRect.top - 30;
1202 const left = targetRect.left + targetRect.width / 2 - 40;
1203
1204 floatingMsg.style.position = "fixed";
1205 floatingMsg.style.top = `${top}px`;
1206 floatingMsg.style.left = `${left}px`;
1207 floatingMsg.style.zIndex = "9999";
1208
1209 // Add to document body
1210 document.body.appendChild(floatingMsg);
1211
1212 // Animate in
1213 floatingMsg.style.opacity = "0";
1214 floatingMsg.style.transform = "translateY(10px)";
1215
1216 setTimeout(() => {
1217 floatingMsg.style.opacity = "1";
1218 floatingMsg.style.transform = "translateY(0)";
1219 }, 10);
1220
1221 // Remove after animation
1222 setTimeout(() => {
1223 floatingMsg.style.opacity = "0";
1224 floatingMsg.style.transform = "translateY(-10px)";
1225
1226 setTimeout(() => {
1227 document.body.removeChild(floatingMsg);
1228 }, 300);
1229 }, 1500);
1230 }
1231
philip.zeyliger6d3de482025-06-10 19:38:14 -07001232 // Format GitHub repository URL to org/repo format
1233 formatGitHubRepo(url) {
1234 if (!url) return null;
1235
1236 // Common GitHub URL patterns
1237 const patterns = [
1238 // HTTPS URLs
1239 /https:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
1240 // SSH URLs
1241 /git@github\.com:([^/]+)\/([^/\s.]+)(?:\.git)?/,
1242 // Git protocol
1243 /git:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
1244 ];
1245
1246 for (const pattern of patterns) {
1247 const match = url.match(pattern);
1248 if (match) {
1249 return {
1250 formatted: `${match[1]}/${match[2]}`,
1251 url: `https://github.com/${match[1]}/${match[2]}`,
1252 owner: match[1],
1253 repo: match[2],
1254 };
1255 }
1256 }
1257
1258 return null;
1259 }
1260
1261 // Generate GitHub branch URL if linking is enabled
1262 getGitHubBranchLink(branchName) {
1263 if (!this.state?.link_to_github || !branchName) {
1264 return null;
1265 }
1266
1267 const github = this.formatGitHubRepo(this.state?.git_origin);
1268 if (!github) {
1269 return null;
1270 }
1271
1272 return `https://github.com/${github.owner}/${github.repo}/tree/${branchName}`;
1273 }
1274
Sean McCullough86b56862025-04-18 13:04:03 -07001275 render() {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001276 // Calculate if this is an end of turn message with no parent conversation ID
1277 const isEndOfTurn =
1278 this.message?.end_of_turn && !this.message?.parent_conversation_id;
1279
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001280 const isPreCompaction =
1281 this.message?.idx !== undefined &&
1282 this.message.idx < this.firstMessageIndex;
1283
Sean McCullough86b56862025-04-18 13:04:03 -07001284 return html`
1285 <div
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001286 class="message ${this.message?.type} ${isEndOfTurn
Sean McCullough86b56862025-04-18 13:04:03 -07001287 ? "end-of-turn"
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001288 : ""} ${isPreCompaction ? "pre-compaction" : ""}"
Sean McCullough86b56862025-04-18 13:04:03 -07001289 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001290 <div class="message-container">
1291 <!-- Left area (empty for simplicity) -->
1292 <div class="message-metadata-left"></div>
1293
1294 <!-- Message bubble -->
1295 <div class="message-bubble-container">
1296 <div class="message-content">
1297 <div class="message-text-container">
1298 <div class="message-actions">
1299 ${copyButton(this.message?.content)}
1300 <button
1301 class="info-icon"
1302 title="Show message details"
1303 @click=${this._toggleInfo}
Sean McCullough71941bd2025-04-18 13:31:48 -07001304 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001305 <svg
1306 xmlns="http://www.w3.org/2000/svg"
1307 width="16"
1308 height="16"
1309 viewBox="0 0 24 24"
1310 fill="none"
1311 stroke="currentColor"
1312 stroke-width="2"
1313 stroke-linecap="round"
1314 stroke-linejoin="round"
1315 >
1316 <circle cx="12" cy="12" r="10"></circle>
1317 <line x1="12" y1="16" x2="12" y2="12"></line>
1318 <line x1="12" y1="8" x2="12.01" y2="8"></line>
1319 </svg>
1320 </button>
1321 </div>
1322 ${this.message?.content
1323 ? html`
1324 <div class="message-text markdown-content">
1325 ${unsafeHTML(
1326 this.renderMarkdown(this.message?.content),
1327 )}
1328 </div>
1329 `
1330 : ""}
1331
1332 <!-- End of turn indicator inside the bubble -->
1333 ${isEndOfTurn && this.message?.elapsed
1334 ? html`
1335 <div class="end-of-turn-indicator">
1336 end of turn
1337 (${this._formatDuration(this.message?.elapsed)})
1338 </div>
1339 `
1340 : ""}
1341
1342 <!-- Info panel that can be toggled -->
1343 ${this.showInfo
1344 ? html`
1345 <div class="message-info-panel">
1346 <div class="info-row">
1347 <span class="info-label">Type:</span>
1348 <span class="info-value">${this.message?.type}</span>
1349 </div>
1350 <div class="info-row">
1351 <span class="info-label">Time:</span>
1352 <span class="info-value"
1353 >${this.formatTimestamp(
1354 this.message?.timestamp,
1355 "",
1356 )}</span
1357 >
1358 </div>
1359 ${this.message?.elapsed
1360 ? html`
1361 <div class="info-row">
1362 <span class="info-label">Duration:</span>
1363 <span class="info-value"
1364 >${this._formatDuration(
1365 this.message?.elapsed,
1366 )}</span
1367 >
1368 </div>
1369 `
1370 : ""}
1371 ${this.message?.usage
1372 ? html`
1373 <div class="info-row">
1374 <span class="info-label">Tokens:</span>
1375 <span class="info-value">
1376 ${this.message?.usage
1377 ? html`
1378 <div>
1379 Input:
1380 ${this.formatNumber(
1381 this.message?.usage?.input_tokens ||
1382 0,
1383 )}
1384 </div>
1385 ${this.message?.usage
1386 ?.cache_creation_input_tokens
1387 ? html`
1388 <div>
1389 Cache creation:
1390 ${this.formatNumber(
1391 this.message?.usage
1392 ?.cache_creation_input_tokens,
1393 )}
1394 </div>
1395 `
1396 : ""}
1397 ${this.message?.usage
1398 ?.cache_read_input_tokens
1399 ? html`
1400 <div>
1401 Cache read:
1402 ${this.formatNumber(
1403 this.message?.usage
1404 ?.cache_read_input_tokens,
1405 )}
1406 </div>
1407 `
1408 : ""}
1409 <div>
1410 Output:
1411 ${this.formatNumber(
1412 this.message?.usage?.output_tokens,
1413 )}
1414 </div>
1415 <div>
1416 Cost:
1417 ${this.formatCurrency(
1418 this.message?.usage?.cost_usd,
1419 )}
1420 </div>
1421 `
1422 : "N/A"}
1423 </span>
1424 </div>
1425 `
1426 : ""}
1427 ${this.message?.conversation_id
1428 ? html`
1429 <div class="info-row">
1430 <span class="info-label">Conversation ID:</span>
1431 <span class="info-value conversation-id"
1432 >${this.message?.conversation_id}</span
1433 >
1434 </div>
1435 `
1436 : ""}
1437 </div>
1438 `
1439 : ""}
1440 </div>
1441
1442 <!-- Tool calls - only shown for agent messages -->
1443 ${this.message?.type === "agent"
1444 ? html`
1445 <sketch-tool-calls
1446 .toolCalls=${this.message?.tool_calls}
1447 .open=${this.open}
1448 ></sketch-tool-calls>
1449 `
1450 : ""}
1451
1452 <!-- Commits section (redesigned as bubbles) -->
1453 ${this.message?.commits
1454 ? html`
1455 <div class="commits-container">
1456 <div class="commit-notification">
1457 ${this.message.commits.length} new
1458 commit${this.message.commits.length > 1 ? "s" : ""}
1459 detected
1460 </div>
1461 ${this.message.commits.map((commit) => {
1462 return html`
1463 <div class="commit-card">
Philip Zeyliger72682df2025-04-23 13:09:46 -07001464 <span
1465 class="commit-hash"
1466 title="Click to copy: ${commit.hash}"
1467 @click=${(e) =>
1468 this.copyToClipboard(
1469 commit.hash.substring(0, 8),
1470 e,
1471 )}
1472 >
Pokey Rule7be879f2025-04-23 15:30:15 +01001473 ${commit.hash.substring(0, 8)}
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001474 </span>
1475 ${commit.pushed_branch
philip.zeyliger6d3de482025-06-10 19:38:14 -07001476 ? (() => {
1477 const githubLink = this.getGitHubBranchLink(
1478 commit.pushed_branch,
1479 );
1480 return html`
1481 <div class="commit-branch-container">
1482 <span
1483 class="commit-branch pushed-branch"
1484 title="Click to copy: ${commit.pushed_branch}"
1485 @click=${(e) =>
1486 this.copyToClipboard(
1487 commit.pushed_branch,
1488 e,
1489 )}
1490 >${commit.pushed_branch}</span
1491 >
cbroebbdee42025-06-20 09:57:44 +00001492 <span
1493 class="copy-icon"
1494 @click=${(e) => {
1495 e.stopPropagation();
1496 this.copyToClipboard(
1497 commit.pushed_branch,
1498 e,
1499 );
1500 }}
1501 >
philip.zeyliger6d3de482025-06-10 19:38:14 -07001502 <svg
1503 xmlns="http://www.w3.org/2000/svg"
1504 width="14"
1505 height="14"
1506 viewBox="0 0 24 24"
1507 fill="none"
1508 stroke="currentColor"
1509 stroke-width="2"
1510 stroke-linecap="round"
1511 stroke-linejoin="round"
1512 >
1513 <rect
1514 x="9"
1515 y="9"
1516 width="13"
1517 height="13"
1518 rx="2"
1519 ry="2"
1520 ></rect>
1521 <path
1522 d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
1523 ></path>
1524 </svg>
1525 </span>
1526 ${githubLink
1527 ? html`
1528 <a
1529 href="${githubLink}"
1530 target="_blank"
1531 rel="noopener noreferrer"
1532 class="octocat-link"
1533 title="Open ${commit.pushed_branch} on GitHub"
1534 @click=${(e) =>
1535 e.stopPropagation()}
1536 >
1537 <svg
1538 class="octocat-icon"
1539 viewBox="0 0 16 16"
1540 width="14"
1541 height="14"
1542 >
1543 <path
1544 fill="currentColor"
1545 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"
1546 />
1547 </svg>
1548 </a>
1549 `
1550 : ""}
1551 </div>
1552 `;
1553 })()
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001554 : ``}
1555 <span class="commit-subject"
1556 >${commit.subject}</span
Sean McCullough71941bd2025-04-18 13:31:48 -07001557 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001558 <button
1559 class="commit-diff-button"
1560 @click=${() => this.showCommit(commit.hash)}
1561 >
1562 View Diff
1563 </button>
Sean McCullough86b56862025-04-18 13:04:03 -07001564 </div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001565 `;
1566 })}
1567 </div>
1568 `
1569 : ""}
1570 </div>
1571 </div>
1572
1573 <!-- Right side (empty for consistency) -->
1574 <div class="message-metadata-right"></div>
Sean McCullough86b56862025-04-18 13:04:03 -07001575 </div>
1576 </div>
1577 `;
1578 }
1579}
1580
Sean McCullough71941bd2025-04-18 13:31:48 -07001581function copyButton(textToCopy: string) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001582 // Use an icon of overlapping rectangles for copy
1583 const buttonClass = "copy-icon";
1584
1585 // SVG for copy icon (two overlapping rectangles)
1586 const copyIcon = html`<svg
1587 xmlns="http://www.w3.org/2000/svg"
1588 width="16"
1589 height="16"
1590 viewBox="0 0 24 24"
1591 fill="none"
1592 stroke="currentColor"
1593 stroke-width="2"
1594 stroke-linecap="round"
1595 stroke-linejoin="round"
1596 >
1597 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1598 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1599 </svg>`;
1600
1601 // SVG for success check mark
1602 const successIcon = html`<svg
1603 xmlns="http://www.w3.org/2000/svg"
1604 width="16"
1605 height="16"
1606 viewBox="0 0 24 24"
1607 fill="none"
1608 stroke="currentColor"
1609 stroke-width="2"
1610 stroke-linecap="round"
1611 stroke-linejoin="round"
1612 >
1613 <path d="M20 6L9 17l-5-5"></path>
1614 </svg>`;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001615
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001616 const ret = html`<button
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001617 class="${buttonClass}"
1618 title="Copy to clipboard"
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001619 @click=${(e: Event) => {
1620 e.stopPropagation();
1621 const copyButton = e.currentTarget as HTMLButtonElement;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001622 const originalInnerHTML = copyButton.innerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001623 navigator.clipboard
1624 .writeText(textToCopy)
1625 .then(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001626 copyButton.innerHTML = "";
1627 const successElement = document.createElement("div");
1628 copyButton.appendChild(successElement);
1629 render(successIcon, successElement);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001630 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001631 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001632 }, 2000);
1633 })
1634 .catch((err) => {
1635 console.error("Failed to copy text: ", err);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001636 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001637 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001638 }, 2000);
1639 });
1640 }}
1641 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001642 ${copyIcon}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001643 </button>`;
Sean McCullough86b56862025-04-18 13:04:03 -07001644
Sean McCullough71941bd2025-04-18 13:31:48 -07001645 return ret;
Sean McCullough86b56862025-04-18 13:04:03 -07001646}
1647
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001648// Create global styles for floating messages
1649const floatingMessageStyles = document.createElement("style");
1650floatingMessageStyles.textContent = `
1651 .floating-message {
1652 background-color: rgba(0, 0, 0, 0.8);
1653 color: white;
1654 padding: 5px 10px;
1655 border-radius: 4px;
1656 font-size: 12px;
1657 font-family: system-ui, sans-serif;
1658 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
1659 pointer-events: none;
1660 transition: opacity 0.3s ease, transform 0.3s ease;
1661 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001662
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001663 .floating-message.success {
1664 background-color: rgba(40, 167, 69, 0.9);
1665 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001666
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001667 .floating-message.error {
1668 background-color: rgba(220, 53, 69, 0.9);
1669 }
Philip Zeyligere31d2a92025-05-11 15:22:35 -07001670
1671 /* Style for code, pre elements, and tool components to ensure proper wrapping/truncation */
1672 pre, code, sketch-tool-calls, sketch-tool-card, sketch-tool-card-bash {
1673 white-space: nowrap;
1674 overflow: hidden;
1675 text-overflow: ellipsis;
1676 max-width: 100%;
1677 }
1678
1679 /* Special rule for the message content container */
1680 .message-content {
1681 max-width: 100% !important;
1682 overflow: hidden !important;
1683 }
1684
1685 /* Ensure tool call containers don't overflow */
1686 ::slotted(sketch-tool-calls) {
1687 max-width: 100%;
1688 width: 100%;
1689 overflow-wrap: break-word;
1690 word-break: break-word;
1691 }
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001692`;
1693document.head.appendChild(floatingMessageStyles);
1694
Sean McCullough86b56862025-04-18 13:04:03 -07001695declare global {
1696 interface HTMLElementTagNameMap {
1697 "sketch-timeline-message": SketchTimelineMessage;
1698 }
1699}