blob: 1fb8334324cbbc6d4f12ca5cb74d8cf3f624908f [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
bankseancad67b02025-06-27 21:57:05 +0000674 /* User name styling - positioned outside and below the message bubble */
675 .user-name-container {
676 display: flex;
677 justify-content: flex-end;
678 margin-top: 4px;
679 padding-right: 80px; /* Account for right metadata area */
680 }
681
682 :host([compactpadding]) .user-name-container {
683 padding-right: 0; /* No right padding in compact mode */
684 }
685
686 .user-name {
687 font-size: 11px;
688 color: #666;
689 font-style: italic;
690 text-align: right;
691 }
692
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000693 .user .message-info-panel {
694 background-color: rgba(255, 255, 255, 0.15);
695 border-left: 2px solid rgba(255, 255, 255, 0.2);
696 }
697
698 .info-row {
699 margin-bottom: 3px;
700 display: flex;
701 }
702
703 .info-label {
704 font-weight: bold;
705 margin-right: 5px;
706 min-width: 60px;
707 }
708
709 .info-value {
710 flex: 1;
711 }
712
713 .conversation-id {
714 font-family: monospace;
Sean McCullough86b56862025-04-18 13:04:03 -0700715 font-size: 10px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000716 word-break: break-all;
Sean McCullough86b56862025-04-18 13:04:03 -0700717 }
718
719 .markdown-content {
720 box-sizing: border-box;
721 min-width: 200px;
722 margin: 0 auto;
723 }
724
725 .markdown-content p {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000726 margin-block-start: 0.3em;
727 margin-block-end: 0.3em;
728 }
729
730 .markdown-content p:first-child {
731 margin-block-start: 0;
732 }
733
734 .markdown-content p:last-child {
735 margin-block-end: 0;
736 }
737
738 /* Styling for markdown elements */
739 .markdown-content a {
740 color: inherit;
741 text-decoration: underline;
742 }
743
744 .user .markdown-content a {
745 color: #fff;
746 text-decoration: underline;
747 }
748
749 .markdown-content ul,
750 .markdown-content ol {
751 padding-left: 1.5em;
752 margin: 0.5em 0;
753 }
754
755 .markdown-content blockquote {
756 border-left: 3px solid rgba(0, 0, 0, 0.2);
757 padding-left: 1em;
758 margin-left: 0.5em;
759 font-style: italic;
760 }
761
762 .user .markdown-content blockquote {
763 border-left: 3px solid rgba(255, 255, 255, 0.4);
Sean McCullough86b56862025-04-18 13:04:03 -0700764 }
Autoformatterdded2d62025-04-28 00:27:21 +0000765
Sean McCullough8d93e362025-04-27 23:32:18 +0000766 /* Mermaid diagram styling */
767 .mermaid-container {
768 margin: 1em 0;
769 padding: 0.5em;
770 background-color: #f8f8f8;
771 border-radius: 4px;
772 overflow-x: auto;
773 }
Autoformatterdded2d62025-04-28 00:27:21 +0000774
Sean McCullough8d93e362025-04-27 23:32:18 +0000775 .mermaid {
776 text-align: center;
777 }
philip.zeyligerffa94c62025-06-19 18:43:37 -0700778
779 /* Print styles for message components */
780 @media print {
781 .message {
782 page-break-inside: avoid;
783 margin-bottom: 12px;
784 }
785
786 .message-container {
787 page-break-inside: avoid;
788 }
789
790 /* Hide copy buttons and interactive elements during printing */
791 .copy-icon,
792 .info-icon,
793 .commit-diff-button {
794 display: none !important;
795 }
796
797 /* Ensure code blocks print properly */
798 .message-content pre {
799 white-space: pre-wrap;
800 word-wrap: break-word;
801 page-break-inside: avoid;
802 background: #f8f8f8 !important;
803 border: 1px solid #ddd !important;
804 padding: 8px !important;
805 }
806
807 /* Ensure tool calls section prints properly */
808 .tool-calls-section {
809 page-break-inside: avoid;
810 }
811
812 /* Simplify message metadata for print */
813 .message-metadata-left {
814 font-size: 10px;
815 }
816
817 /* Ensure content doesn't break poorly */
818 .message-content {
819 orphans: 3;
820 widows: 3;
821 }
822
823 /* Hide floating messages during print */
824 .floating-message {
825 display: none !important;
826 }
827 }
Sean McCullough86b56862025-04-18 13:04:03 -0700828 `;
829
Sean McCullough8d93e362025-04-27 23:32:18 +0000830 // Track mermaid diagrams that need rendering
831 private mermaidDiagrams = new Map();
832
Sean McCullough86b56862025-04-18 13:04:03 -0700833 constructor() {
834 super();
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000835 // Mermaid will be initialized lazily when first needed
Sean McCullough86b56862025-04-18 13:04:03 -0700836 }
837
838 // See https://lit.dev/docs/components/lifecycle/
839 connectedCallback() {
840 super.connectedCallback();
841 }
Autoformatterdded2d62025-04-28 00:27:21 +0000842
Sean McCullough8d93e362025-04-27 23:32:18 +0000843 // After the component is updated and rendered, render any mermaid diagrams
844 updated(changedProperties: Map<string, unknown>) {
845 super.updated(changedProperties);
846 this.renderMermaidDiagrams();
Pokey Rulea10f1512025-05-15 13:53:26 +0000847 this.setupCodeBlockCopyButtons();
Sean McCullough8d93e362025-04-27 23:32:18 +0000848 }
Autoformatterdded2d62025-04-28 00:27:21 +0000849
Sean McCullough8d93e362025-04-27 23:32:18 +0000850 // Render mermaid diagrams after the component is updated
851 renderMermaidDiagrams() {
852 // Add a small delay to ensure the DOM is fully rendered
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000853 setTimeout(async () => {
Sean McCullough8d93e362025-04-27 23:32:18 +0000854 // Find all mermaid containers in our shadow root
Autoformatterdded2d62025-04-28 00:27:21 +0000855 const containers = this.shadowRoot?.querySelectorAll(".mermaid");
Sean McCullough8d93e362025-04-27 23:32:18 +0000856 if (!containers || containers.length === 0) return;
Autoformatterdded2d62025-04-28 00:27:21 +0000857
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000858 try {
859 // Load mermaid dynamically
860 const mermaidLib = await loadMermaid();
Autoformatterdded2d62025-04-28 00:27:21 +0000861
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000862 // Initialize mermaid with specific config (only once per load)
863 mermaidLib.initialize({
864 startOnLoad: false,
865 suppressErrorRendering: true,
866 theme: "default",
867 securityLevel: "loose", // Allows more flexibility but be careful with user-generated content
868 fontFamily: "monospace",
869 });
Autoformatterdded2d62025-04-28 00:27:21 +0000870
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000871 // Process each mermaid diagram
872 containers.forEach((container) => {
873 const id = container.id;
874 const code = container.textContent || "";
875 if (!code || !id) return; // Use return for forEach instead of continue
876
877 try {
878 // Clear any previous content
879 container.innerHTML = code;
880
881 // Render the mermaid diagram using promise
882 mermaidLib
883 .render(`${id}-svg`, code)
884 .then(({ svg }) => {
885 container.innerHTML = svg;
886 })
887 .catch((err) => {
888 console.error("Error rendering mermaid diagram:", err);
889 // Show the original code as fallback
890 container.innerHTML = `<pre>${code}</pre>`;
891 });
892 } catch (err) {
893 console.error("Error processing mermaid diagram:", err);
894 // Show the original code as fallback
895 container.innerHTML = `<pre>${code}</pre>`;
896 }
897 });
898 } catch (err) {
899 console.error("Error loading mermaid:", err);
900 // Show the original code as fallback for all diagrams
901 containers.forEach((container) => {
902 const code = container.textContent || "";
Sean McCullough8d93e362025-04-27 23:32:18 +0000903 container.innerHTML = `<pre>${code}</pre>`;
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000904 });
905 }
Sean McCullough8d93e362025-04-27 23:32:18 +0000906 }, 100); // Small delay to ensure DOM is ready
907 }
Sean McCullough86b56862025-04-18 13:04:03 -0700908
Pokey Rulea10f1512025-05-15 13:53:26 +0000909 // Setup code block copy buttons after component is updated
910 setupCodeBlockCopyButtons() {
911 setTimeout(() => {
912 // Find all copy buttons in code blocks
913 const copyButtons =
914 this.shadowRoot?.querySelectorAll(".code-copy-button");
915 if (!copyButtons || copyButtons.length === 0) return;
916
917 // Add click event listener to each button
918 copyButtons.forEach((button) => {
919 button.addEventListener("click", (e) => {
920 e.stopPropagation();
921 const codeId = (button as HTMLElement).dataset.codeId;
922 if (!codeId) return;
923
924 const codeElement = this.shadowRoot?.querySelector(`#${codeId}`);
925 if (!codeElement) return;
926
927 const codeText = codeElement.textContent || "";
928 const buttonRect = button.getBoundingClientRect();
929
930 // Copy code to clipboard
931 navigator.clipboard
932 .writeText(codeText)
933 .then(() => {
934 // Show success indicator
935 const originalHTML = button.innerHTML;
936 button.innerHTML = `
937 <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">
938 <path d="M20 6L9 17l-5-5"></path>
939 </svg>
940 `;
941
942 // Display floating message
943 this.showFloatingMessage("Copied!", buttonRect, "success");
944
945 // Reset button after delay
946 setTimeout(() => {
947 button.innerHTML = originalHTML;
948 }, 2000);
949 })
950 .catch((err) => {
951 console.error("Failed to copy code:", err);
952 this.showFloatingMessage("Failed to copy!", buttonRect, "error");
953 });
954 });
955 });
956 }, 100); // Small delay to ensure DOM is ready
957 }
958
Sean McCullough86b56862025-04-18 13:04:03 -0700959 // See https://lit.dev/docs/components/lifecycle/
960 disconnectedCallback() {
961 super.disconnectedCallback();
962 }
963
964 renderMarkdown(markdownContent: string): string {
965 try {
Sean McCullough8d93e362025-04-27 23:32:18 +0000966 // Create a custom renderer
967 const renderer = new Renderer();
968 const originalCodeRenderer = renderer.code.bind(renderer);
Autoformatterdded2d62025-04-28 00:27:21 +0000969
Pokey Rulea10f1512025-05-15 13:53:26 +0000970 // Override the code renderer to handle mermaid diagrams and add copy buttons
Autoformatterdded2d62025-04-28 00:27:21 +0000971 renderer.code = function ({ text, lang, escaped }: Tokens.Code): string {
972 if (lang === "mermaid") {
Sean McCullough8d93e362025-04-27 23:32:18 +0000973 // Generate a unique ID for this diagram
974 const id = `mermaid-diagram-${Math.random().toString(36).substring(2, 10)}`;
Autoformatterdded2d62025-04-28 00:27:21 +0000975
Sean McCullough8d93e362025-04-27 23:32:18 +0000976 // Just create the container and mermaid div - we'll render it in the updated() lifecycle method
977 return `<div class="mermaid-container">
978 <div class="mermaid" id="${id}">${text}</div>
979 </div>`;
980 }
Pokey Rulea10f1512025-05-15 13:53:26 +0000981
Philip Zeyliger0d092842025-06-09 18:57:12 -0700982 // For regular code blocks, call the original renderer to get properly escaped HTML
983 const originalCodeHtml = originalCodeRenderer({ text, lang, escaped });
984
985 // Extract the code content from the original HTML to add our custom wrapper
986 // The original renderer returns: <pre><code class="language-x">escapedText</code></pre>
987 const codeMatch = originalCodeHtml.match(
988 /<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/,
989 );
990 if (!codeMatch) {
991 // Fallback to original if we can't parse it
992 return originalCodeHtml;
993 }
994
995 const escapedText = codeMatch[1];
Pokey Rulea10f1512025-05-15 13:53:26 +0000996 const id = `code-block-${Math.random().toString(36).substring(2, 10)}`;
997 const langClass = lang ? ` class="language-${lang}"` : "";
998
999 return `<div class="code-block-container">
1000 <div class="code-block-header">
1001 ${lang ? `<span class="code-language">${lang}</span>` : ""}
1002 <button class="code-copy-button" title="Copy code" data-code-id="${id}">
1003 <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">
1004 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1005 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1006 </svg>
1007 </button>
1008 </div>
Philip Zeyliger0d092842025-06-09 18:57:12 -07001009 <pre><code id="${id}"${langClass}>${escapedText}</code></pre>
Pokey Rulea10f1512025-05-15 13:53:26 +00001010 </div>`;
Sean McCullough8d93e362025-04-27 23:32:18 +00001011 };
Autoformatterdded2d62025-04-28 00:27:21 +00001012
Philip Zeyliger53ab2452025-06-04 17:49:33 +00001013 // Set markdown options for proper code block highlighting
Sean McCullough86b56862025-04-18 13:04:03 -07001014 const markedOptions: MarkedOptions = {
1015 gfm: true, // GitHub Flavored Markdown
1016 breaks: true, // Convert newlines to <br>
1017 async: false,
Autoformatterdded2d62025-04-28 00:27:21 +00001018 renderer: renderer,
Sean McCullough86b56862025-04-18 13:04:03 -07001019 };
Philip Zeyliger53ab2452025-06-04 17:49:33 +00001020
1021 // Parse markdown and sanitize the output HTML with DOMPurify
1022 const htmlOutput = marked.parse(markdownContent, markedOptions) as string;
1023 return DOMPurify.sanitize(htmlOutput, {
1024 // Allow common HTML elements that are safe
1025 ALLOWED_TAGS: [
1026 "p",
1027 "br",
1028 "strong",
1029 "em",
1030 "b",
1031 "i",
1032 "u",
1033 "s",
1034 "code",
1035 "pre",
1036 "h1",
1037 "h2",
1038 "h3",
1039 "h4",
1040 "h5",
1041 "h6",
1042 "ul",
1043 "ol",
1044 "li",
1045 "blockquote",
1046 "a",
1047 "div",
1048 "span", // For mermaid diagrams and code blocks
1049 "svg",
1050 "g",
1051 "path",
1052 "rect",
1053 "circle",
1054 "text",
1055 "line",
1056 "polygon", // For mermaid SVG
1057 "button", // For code copy buttons
1058 ],
1059 ALLOWED_ATTR: [
1060 "href",
1061 "title",
1062 "target",
1063 "rel", // For links
1064 "class",
1065 "id", // For styling and functionality
1066 "data-*", // For code copy buttons
1067 // SVG attributes for mermaid diagrams
1068 "viewBox",
1069 "width",
1070 "height",
1071 "xmlns",
1072 "fill",
1073 "stroke",
1074 "stroke-width",
1075 "d",
1076 "x",
1077 "y",
1078 "x1",
1079 "y1",
1080 "x2",
1081 "y2",
1082 "cx",
1083 "cy",
1084 "r",
1085 "rx",
1086 "ry",
1087 "points",
1088 "transform",
1089 "text-anchor",
1090 "font-size",
1091 "font-family",
1092 ],
1093 // Allow data attributes for functionality
1094 ALLOW_DATA_ATTR: true,
1095 // Keep whitespace for code formatting
1096 KEEP_CONTENT: true,
1097 });
Sean McCullough86b56862025-04-18 13:04:03 -07001098 } catch (error) {
1099 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +00001100 // Fallback to sanitized plain text if markdown parsing fails
1101 return DOMPurify.sanitize(markdownContent);
Sean McCullough86b56862025-04-18 13:04:03 -07001102 }
1103 }
1104
1105 /**
1106 * Format timestamp for display
1107 */
1108 formatTimestamp(
1109 timestamp: string | number | Date | null | undefined,
1110 defaultValue: string = "",
1111 ): string {
1112 if (!timestamp) return defaultValue;
1113 try {
1114 const date = new Date(timestamp);
1115 if (isNaN(date.getTime())) return defaultValue;
1116
1117 // Format: Mar 13, 2025 09:53:25 AM
1118 return date.toLocaleString("en-US", {
1119 month: "short",
1120 day: "numeric",
1121 year: "numeric",
1122 hour: "numeric",
1123 minute: "2-digit",
1124 second: "2-digit",
1125 hour12: true,
1126 });
1127 } catch (e) {
1128 return defaultValue;
1129 }
1130 }
1131
1132 formatNumber(
1133 num: number | null | undefined,
1134 defaultValue: string = "0",
1135 ): string {
1136 if (num === undefined || num === null) return defaultValue;
1137 try {
1138 return num.toLocaleString();
1139 } catch (e) {
1140 return String(num);
1141 }
1142 }
1143 formatCurrency(
1144 num: number | string | null | undefined,
1145 defaultValue: string = "$0.00",
1146 isMessageLevel: boolean = false,
1147 ): string {
1148 if (num === undefined || num === null) return defaultValue;
1149 try {
1150 // Use 4 decimal places for message-level costs, 2 for totals
1151 const decimalPlaces = isMessageLevel ? 4 : 2;
1152 return `$${parseFloat(String(num)).toFixed(decimalPlaces)}`;
1153 } catch (e) {
1154 return defaultValue;
1155 }
1156 }
1157
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001158 // Format duration from nanoseconds to a human-readable string
1159 _formatDuration(nanoseconds: number | null | undefined): string {
1160 if (!nanoseconds) return "0s";
1161
1162 const seconds = nanoseconds / 1e9;
1163
1164 if (seconds < 60) {
1165 return `${seconds.toFixed(1)}s`;
1166 } else if (seconds < 3600) {
1167 const minutes = Math.floor(seconds / 60);
1168 const remainingSeconds = seconds % 60;
1169 return `${minutes}min ${remainingSeconds.toFixed(0)}s`;
1170 } else {
1171 const hours = Math.floor(seconds / 3600);
1172 const remainingSeconds = seconds % 3600;
1173 const minutes = Math.floor(remainingSeconds / 60);
1174 return `${hours}h ${minutes}min`;
1175 }
1176 }
1177
Sean McCullough86b56862025-04-18 13:04:03 -07001178 showCommit(commitHash: string) {
Sean McCullough71941bd2025-04-18 13:31:48 -07001179 this.dispatchEvent(
1180 new CustomEvent("show-commit-diff", {
1181 bubbles: true,
1182 composed: true,
1183 detail: { commitHash },
1184 }),
1185 );
Sean McCullough86b56862025-04-18 13:04:03 -07001186 }
1187
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001188 _toggleInfo(e: Event) {
1189 e.stopPropagation();
1190 this.showInfo = !this.showInfo;
1191 }
1192
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001193 copyToClipboard(text: string, event: Event) {
1194 const element = event.currentTarget as HTMLElement;
1195 const rect = element.getBoundingClientRect();
1196
1197 navigator.clipboard
1198 .writeText(text)
1199 .then(() => {
1200 this.showFloatingMessage("Copied!", rect, "success");
1201 })
1202 .catch((err) => {
1203 console.error("Failed to copy text: ", err);
1204 this.showFloatingMessage("Failed to copy!", rect, "error");
1205 });
1206 }
1207
1208 showFloatingMessage(
1209 message: string,
1210 targetRect: DOMRect,
1211 type: "success" | "error",
1212 ) {
1213 // Create floating message element
1214 const floatingMsg = document.createElement("div");
1215 floatingMsg.textContent = message;
1216 floatingMsg.className = `floating-message ${type}`;
1217
1218 // Position it near the clicked element
1219 // Position just above the element
1220 const top = targetRect.top - 30;
1221 const left = targetRect.left + targetRect.width / 2 - 40;
1222
1223 floatingMsg.style.position = "fixed";
1224 floatingMsg.style.top = `${top}px`;
1225 floatingMsg.style.left = `${left}px`;
1226 floatingMsg.style.zIndex = "9999";
1227
1228 // Add to document body
1229 document.body.appendChild(floatingMsg);
1230
1231 // Animate in
1232 floatingMsg.style.opacity = "0";
1233 floatingMsg.style.transform = "translateY(10px)";
1234
1235 setTimeout(() => {
1236 floatingMsg.style.opacity = "1";
1237 floatingMsg.style.transform = "translateY(0)";
1238 }, 10);
1239
1240 // Remove after animation
1241 setTimeout(() => {
1242 floatingMsg.style.opacity = "0";
1243 floatingMsg.style.transform = "translateY(-10px)";
1244
1245 setTimeout(() => {
1246 document.body.removeChild(floatingMsg);
1247 }, 300);
1248 }, 1500);
1249 }
1250
philip.zeyliger6d3de482025-06-10 19:38:14 -07001251 // Format GitHub repository URL to org/repo format
1252 formatGitHubRepo(url) {
1253 if (!url) return null;
1254
1255 // Common GitHub URL patterns
1256 const patterns = [
1257 // HTTPS URLs
1258 /https:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
1259 // SSH URLs
1260 /git@github\.com:([^/]+)\/([^/\s.]+)(?:\.git)?/,
1261 // Git protocol
1262 /git:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
1263 ];
1264
1265 for (const pattern of patterns) {
1266 const match = url.match(pattern);
1267 if (match) {
1268 return {
1269 formatted: `${match[1]}/${match[2]}`,
1270 url: `https://github.com/${match[1]}/${match[2]}`,
1271 owner: match[1],
1272 repo: match[2],
1273 };
1274 }
1275 }
1276
1277 return null;
1278 }
1279
1280 // Generate GitHub branch URL if linking is enabled
1281 getGitHubBranchLink(branchName) {
1282 if (!this.state?.link_to_github || !branchName) {
1283 return null;
1284 }
1285
1286 const github = this.formatGitHubRepo(this.state?.git_origin);
1287 if (!github) {
1288 return null;
1289 }
1290
1291 return `https://github.com/${github.owner}/${github.repo}/tree/${branchName}`;
1292 }
1293
Sean McCullough86b56862025-04-18 13:04:03 -07001294 render() {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001295 // Calculate if this is an end of turn message with no parent conversation ID
1296 const isEndOfTurn =
1297 this.message?.end_of_turn && !this.message?.parent_conversation_id;
1298
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001299 const isPreCompaction =
1300 this.message?.idx !== undefined &&
1301 this.message.idx < this.firstMessageIndex;
1302
Sean McCullough86b56862025-04-18 13:04:03 -07001303 return html`
1304 <div
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001305 class="message ${this.message?.type} ${isEndOfTurn
Sean McCullough86b56862025-04-18 13:04:03 -07001306 ? "end-of-turn"
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001307 : ""} ${isPreCompaction ? "pre-compaction" : ""}"
Sean McCullough86b56862025-04-18 13:04:03 -07001308 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001309 <div class="message-container">
1310 <!-- Left area (empty for simplicity) -->
1311 <div class="message-metadata-left"></div>
1312
1313 <!-- Message bubble -->
1314 <div class="message-bubble-container">
1315 <div class="message-content">
1316 <div class="message-text-container">
1317 <div class="message-actions">
1318 ${copyButton(this.message?.content)}
1319 <button
1320 class="info-icon"
1321 title="Show message details"
1322 @click=${this._toggleInfo}
Sean McCullough71941bd2025-04-18 13:31:48 -07001323 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001324 <svg
1325 xmlns="http://www.w3.org/2000/svg"
1326 width="16"
1327 height="16"
1328 viewBox="0 0 24 24"
1329 fill="none"
1330 stroke="currentColor"
1331 stroke-width="2"
1332 stroke-linecap="round"
1333 stroke-linejoin="round"
1334 >
1335 <circle cx="12" cy="12" r="10"></circle>
1336 <line x1="12" y1="16" x2="12" y2="12"></line>
1337 <line x1="12" y1="8" x2="12.01" y2="8"></line>
1338 </svg>
1339 </button>
1340 </div>
1341 ${this.message?.content
1342 ? html`
1343 <div class="message-text markdown-content">
1344 ${unsafeHTML(
1345 this.renderMarkdown(this.message?.content),
1346 )}
1347 </div>
1348 `
1349 : ""}
1350
1351 <!-- End of turn indicator inside the bubble -->
1352 ${isEndOfTurn && this.message?.elapsed
1353 ? html`
1354 <div class="end-of-turn-indicator">
1355 end of turn
1356 (${this._formatDuration(this.message?.elapsed)})
1357 </div>
1358 `
1359 : ""}
1360
1361 <!-- Info panel that can be toggled -->
1362 ${this.showInfo
1363 ? html`
1364 <div class="message-info-panel">
1365 <div class="info-row">
1366 <span class="info-label">Type:</span>
1367 <span class="info-value">${this.message?.type}</span>
1368 </div>
1369 <div class="info-row">
1370 <span class="info-label">Time:</span>
1371 <span class="info-value"
1372 >${this.formatTimestamp(
1373 this.message?.timestamp,
1374 "",
1375 )}</span
1376 >
1377 </div>
1378 ${this.message?.elapsed
1379 ? html`
1380 <div class="info-row">
1381 <span class="info-label">Duration:</span>
1382 <span class="info-value"
1383 >${this._formatDuration(
1384 this.message?.elapsed,
1385 )}</span
1386 >
1387 </div>
1388 `
1389 : ""}
1390 ${this.message?.usage
1391 ? html`
1392 <div class="info-row">
1393 <span class="info-label">Tokens:</span>
1394 <span class="info-value">
1395 ${this.message?.usage
1396 ? html`
1397 <div>
1398 Input:
1399 ${this.formatNumber(
1400 this.message?.usage?.input_tokens ||
1401 0,
1402 )}
1403 </div>
1404 ${this.message?.usage
1405 ?.cache_creation_input_tokens
1406 ? html`
1407 <div>
1408 Cache creation:
1409 ${this.formatNumber(
1410 this.message?.usage
1411 ?.cache_creation_input_tokens,
1412 )}
1413 </div>
1414 `
1415 : ""}
1416 ${this.message?.usage
1417 ?.cache_read_input_tokens
1418 ? html`
1419 <div>
1420 Cache read:
1421 ${this.formatNumber(
1422 this.message?.usage
1423 ?.cache_read_input_tokens,
1424 )}
1425 </div>
1426 `
1427 : ""}
1428 <div>
1429 Output:
1430 ${this.formatNumber(
1431 this.message?.usage?.output_tokens,
1432 )}
1433 </div>
1434 <div>
1435 Cost:
1436 ${this.formatCurrency(
1437 this.message?.usage?.cost_usd,
1438 )}
1439 </div>
1440 `
1441 : "N/A"}
1442 </span>
1443 </div>
1444 `
1445 : ""}
1446 ${this.message?.conversation_id
1447 ? html`
1448 <div class="info-row">
1449 <span class="info-label">Conversation ID:</span>
1450 <span class="info-value conversation-id"
1451 >${this.message?.conversation_id}</span
1452 >
1453 </div>
1454 `
1455 : ""}
1456 </div>
1457 `
1458 : ""}
1459 </div>
1460
1461 <!-- Tool calls - only shown for agent messages -->
1462 ${this.message?.type === "agent"
1463 ? html`
1464 <sketch-tool-calls
1465 .toolCalls=${this.message?.tool_calls}
1466 .open=${this.open}
1467 ></sketch-tool-calls>
1468 `
1469 : ""}
1470
1471 <!-- Commits section (redesigned as bubbles) -->
1472 ${this.message?.commits
1473 ? html`
1474 <div class="commits-container">
1475 <div class="commit-notification">
1476 ${this.message.commits.length} new
1477 commit${this.message.commits.length > 1 ? "s" : ""}
1478 detected
1479 </div>
1480 ${this.message.commits.map((commit) => {
1481 return html`
1482 <div class="commit-card">
Philip Zeyliger72682df2025-04-23 13:09:46 -07001483 <span
1484 class="commit-hash"
1485 title="Click to copy: ${commit.hash}"
1486 @click=${(e) =>
1487 this.copyToClipboard(
1488 commit.hash.substring(0, 8),
1489 e,
1490 )}
1491 >
Pokey Rule7be879f2025-04-23 15:30:15 +01001492 ${commit.hash.substring(0, 8)}
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001493 </span>
1494 ${commit.pushed_branch
philip.zeyliger6d3de482025-06-10 19:38:14 -07001495 ? (() => {
1496 const githubLink = this.getGitHubBranchLink(
1497 commit.pushed_branch,
1498 );
1499 return html`
1500 <div class="commit-branch-container">
1501 <span
1502 class="commit-branch pushed-branch"
1503 title="Click to copy: ${commit.pushed_branch}"
1504 @click=${(e) =>
1505 this.copyToClipboard(
1506 commit.pushed_branch,
1507 e,
1508 )}
1509 >${commit.pushed_branch}</span
1510 >
cbroebbdee42025-06-20 09:57:44 +00001511 <span
1512 class="copy-icon"
1513 @click=${(e) => {
1514 e.stopPropagation();
1515 this.copyToClipboard(
1516 commit.pushed_branch,
1517 e,
1518 );
1519 }}
1520 >
philip.zeyliger6d3de482025-06-10 19:38:14 -07001521 <svg
1522 xmlns="http://www.w3.org/2000/svg"
1523 width="14"
1524 height="14"
1525 viewBox="0 0 24 24"
1526 fill="none"
1527 stroke="currentColor"
1528 stroke-width="2"
1529 stroke-linecap="round"
1530 stroke-linejoin="round"
1531 >
1532 <rect
1533 x="9"
1534 y="9"
1535 width="13"
1536 height="13"
1537 rx="2"
1538 ry="2"
1539 ></rect>
1540 <path
1541 d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
1542 ></path>
1543 </svg>
1544 </span>
1545 ${githubLink
1546 ? html`
1547 <a
1548 href="${githubLink}"
1549 target="_blank"
1550 rel="noopener noreferrer"
1551 class="octocat-link"
1552 title="Open ${commit.pushed_branch} on GitHub"
1553 @click=${(e) =>
1554 e.stopPropagation()}
1555 >
1556 <svg
1557 class="octocat-icon"
1558 viewBox="0 0 16 16"
1559 width="14"
1560 height="14"
1561 >
1562 <path
1563 fill="currentColor"
1564 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"
1565 />
1566 </svg>
1567 </a>
1568 `
1569 : ""}
1570 </div>
1571 `;
1572 })()
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001573 : ``}
1574 <span class="commit-subject"
1575 >${commit.subject}</span
Sean McCullough71941bd2025-04-18 13:31:48 -07001576 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001577 <button
1578 class="commit-diff-button"
1579 @click=${() => this.showCommit(commit.hash)}
1580 >
1581 View Diff
1582 </button>
Sean McCullough86b56862025-04-18 13:04:03 -07001583 </div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001584 `;
1585 })}
1586 </div>
1587 `
1588 : ""}
1589 </div>
1590 </div>
1591
1592 <!-- Right side (empty for consistency) -->
1593 <div class="message-metadata-right"></div>
Sean McCullough86b56862025-04-18 13:04:03 -07001594 </div>
bankseancad67b02025-06-27 21:57:05 +00001595
1596 <!-- User name for user messages - positioned outside and below the bubble -->
1597 ${this.message?.type === "user" && this.state?.git_username
1598 ? html`
1599 <div class="user-name-container">
1600 <div class="user-name">${this.state.git_username}</div>
1601 </div>
1602 `
1603 : ""}
Sean McCullough86b56862025-04-18 13:04:03 -07001604 </div>
1605 `;
1606 }
1607}
1608
Sean McCullough71941bd2025-04-18 13:31:48 -07001609function copyButton(textToCopy: string) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001610 // Use an icon of overlapping rectangles for copy
1611 const buttonClass = "copy-icon";
1612
1613 // SVG for copy icon (two overlapping rectangles)
1614 const copyIcon = html`<svg
1615 xmlns="http://www.w3.org/2000/svg"
1616 width="16"
1617 height="16"
1618 viewBox="0 0 24 24"
1619 fill="none"
1620 stroke="currentColor"
1621 stroke-width="2"
1622 stroke-linecap="round"
1623 stroke-linejoin="round"
1624 >
1625 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1626 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1627 </svg>`;
1628
1629 // SVG for success check mark
1630 const successIcon = html`<svg
1631 xmlns="http://www.w3.org/2000/svg"
1632 width="16"
1633 height="16"
1634 viewBox="0 0 24 24"
1635 fill="none"
1636 stroke="currentColor"
1637 stroke-width="2"
1638 stroke-linecap="round"
1639 stroke-linejoin="round"
1640 >
1641 <path d="M20 6L9 17l-5-5"></path>
1642 </svg>`;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001643
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001644 const ret = html`<button
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001645 class="${buttonClass}"
1646 title="Copy to clipboard"
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001647 @click=${(e: Event) => {
1648 e.stopPropagation();
1649 const copyButton = e.currentTarget as HTMLButtonElement;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001650 const originalInnerHTML = copyButton.innerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001651 navigator.clipboard
1652 .writeText(textToCopy)
1653 .then(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001654 copyButton.innerHTML = "";
1655 const successElement = document.createElement("div");
1656 copyButton.appendChild(successElement);
1657 render(successIcon, successElement);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001658 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001659 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001660 }, 2000);
1661 })
1662 .catch((err) => {
1663 console.error("Failed to copy text: ", err);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001664 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001665 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001666 }, 2000);
1667 });
1668 }}
1669 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001670 ${copyIcon}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001671 </button>`;
Sean McCullough86b56862025-04-18 13:04:03 -07001672
Sean McCullough71941bd2025-04-18 13:31:48 -07001673 return ret;
Sean McCullough86b56862025-04-18 13:04:03 -07001674}
1675
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001676// Create global styles for floating messages
1677const floatingMessageStyles = document.createElement("style");
1678floatingMessageStyles.textContent = `
1679 .floating-message {
1680 background-color: rgba(0, 0, 0, 0.8);
1681 color: white;
1682 padding: 5px 10px;
1683 border-radius: 4px;
1684 font-size: 12px;
1685 font-family: system-ui, sans-serif;
1686 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
1687 pointer-events: none;
1688 transition: opacity 0.3s ease, transform 0.3s ease;
1689 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001690
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001691 .floating-message.success {
1692 background-color: rgba(40, 167, 69, 0.9);
1693 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001694
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001695 .floating-message.error {
1696 background-color: rgba(220, 53, 69, 0.9);
1697 }
Philip Zeyligere31d2a92025-05-11 15:22:35 -07001698
1699 /* Style for code, pre elements, and tool components to ensure proper wrapping/truncation */
1700 pre, code, sketch-tool-calls, sketch-tool-card, sketch-tool-card-bash {
1701 white-space: nowrap;
1702 overflow: hidden;
1703 text-overflow: ellipsis;
1704 max-width: 100%;
1705 }
1706
1707 /* Special rule for the message content container */
1708 .message-content {
1709 max-width: 100% !important;
1710 overflow: hidden !important;
1711 }
1712
1713 /* Ensure tool call containers don't overflow */
1714 ::slotted(sketch-tool-calls) {
1715 max-width: 100%;
1716 width: 100%;
1717 overflow-wrap: break-word;
1718 word-break: break-word;
1719 }
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001720`;
1721document.head.appendChild(floatingMessageStyles);
1722
Sean McCullough86b56862025-04-18 13:04:03 -07001723declare global {
1724 interface HTMLElementTagNameMap {
1725 "sketch-timeline-message": SketchTimelineMessage;
1726 }
1727}