blob: 695ef79b824c501cf5a620299ae1eddcd17f863f [file] [log] [blame]
philip.zeyliger26bc6592025-06-30 20:15:30 -07001/* eslint-disable @typescript-eslint/no-explicit-any */
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00002import { css, html, LitElement, render } from "lit";
Sean McCullough86b56862025-04-18 13:04:03 -07003import { unsafeHTML } from "lit/directives/unsafe-html.js";
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00004import { customElement, property, state } from "lit/decorators.js";
philip.zeyliger6d3de482025-06-10 19:38:14 -07005import { AgentMessage, State } from "../types";
Sean McCullough8d93e362025-04-27 23:32:18 +00006import { marked, MarkedOptions, Renderer, Tokens } from "marked";
philip.zeyliger7c1a6872025-06-16 03:54:37 +00007import type mermaid from "mermaid";
Philip Zeyliger53ab2452025-06-04 17:49:33 +00008import DOMPurify from "dompurify";
philip.zeyliger7c1a6872025-06-16 03:54:37 +00009
10// Mermaid is loaded dynamically - see loadMermaid() function
11declare global {
12 interface Window {
13 mermaid?: typeof mermaid;
14 }
15}
16
17// Mermaid hash will be injected at build time
18declare const __MERMAID_HASH__: string;
19
20// Load Mermaid dynamically
21let mermaidLoadPromise: Promise<any> | null = null;
22
23function loadMermaid(): Promise<typeof mermaid> {
24 if (mermaidLoadPromise) {
25 return mermaidLoadPromise;
26 }
27
28 if (window.mermaid) {
29 return Promise.resolve(window.mermaid);
30 }
31
32 mermaidLoadPromise = new Promise((resolve, reject) => {
33 // Get the Mermaid hash from build-time constant
34 const mermaidHash = __MERMAID_HASH__;
35
36 // Try to load the external Mermaid bundle
37 const script = document.createElement("script");
38 script.onload = () => {
39 // The Mermaid bundle should set window.mermaid
40 if (window.mermaid) {
41 resolve(window.mermaid);
42 } else {
43 reject(new Error("Mermaid not loaded from external bundle"));
44 }
45 };
46 script.onerror = (error) => {
47 console.warn("Failed to load external Mermaid bundle:", error);
48 reject(new Error("Mermaid external bundle failed to load"));
49 };
50
51 // Don't set type="module" since we're using IIFE format
52 script.src = `./static/mermaid-standalone-${mermaidHash}.js`;
53 document.head.appendChild(script);
54 });
55
56 return mermaidLoadPromise;
57}
Sean McCullough86b56862025-04-18 13:04:03 -070058import "./sketch-tool-calls";
59@customElement("sketch-timeline-message")
60export class SketchTimelineMessage extends LitElement {
61 @property()
Sean McCulloughd9f13372025-04-21 15:08:49 -070062 message: AgentMessage;
Sean McCullough86b56862025-04-18 13:04:03 -070063
64 @property()
philip.zeyliger6d3de482025-06-10 19:38:14 -070065 state: State;
66
67 @property()
Sean McCulloughd9f13372025-04-21 15:08:49 -070068 previousMessage: AgentMessage;
Sean McCullough86b56862025-04-18 13:04:03 -070069
Sean McCullough2deac842025-04-21 18:17:57 -070070 @property()
71 open: boolean = false;
72
Philip Zeyligerb8a8f352025-06-02 07:39:37 -070073 @property()
74 firstMessageIndex: number = 0;
75
David Crawshaw4b644682025-06-26 17:15:10 +000076 @property({ type: Boolean, reflect: true, attribute: "compactpadding" })
77 compactPadding: boolean = false;
78
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000079 @state()
80 showInfo: boolean = false;
81
Sean McCullough86b56862025-04-18 13:04:03 -070082 // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
83 // Note that these styles only apply to the scope of this web component's
84 // shadow DOM node, so they won't leak out or collide with CSS declared in
85 // other components or the containing web page (...unless you want it to do that).
86 static styles = css`
87 .message {
88 position: relative;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000089 margin-bottom: 6px;
90 display: flex;
91 flex-direction: column;
92 width: 100%;
Sean McCullough86b56862025-04-18 13:04:03 -070093 }
94
Philip Zeyliger16fa8b42025-05-02 04:28:16 +000095 .message-container {
96 display: flex;
97 position: relative;
98 width: 100%;
99 }
100
101 .message-metadata-left {
102 flex: 0 0 80px;
103 padding: 3px 5px;
104 text-align: right;
105 font-size: 11px;
106 color: #777;
107 align-self: flex-start;
108 }
109
110 .message-metadata-right {
111 flex: 0 0 80px;
112 padding: 3px 5px;
113 text-align: left;
114 font-size: 11px;
115 color: #777;
116 align-self: flex-start;
117 }
118
119 .message-bubble-container {
120 flex: 1;
121 display: flex;
122 max-width: calc(100% - 160px);
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700123 overflow: hidden;
124 text-overflow: ellipsis;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000125 }
126
David Crawshaw4b644682025-06-26 17:15:10 +0000127 :host([compactpadding]) .message-bubble-container {
128 max-width: 100%;
129 }
130
131 :host([compactpadding]) .message-metadata-left,
132 :host([compactpadding]) .message-metadata-right {
133 display: none;
134 }
135
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000136 .user .message-bubble-container {
137 justify-content: flex-end;
138 }
139
140 .agent .message-bubble-container,
141 .tool .message-bubble-container,
142 .error .message-bubble-container {
143 justify-content: flex-start;
Sean McCullough86b56862025-04-18 13:04:03 -0700144 }
145
146 .message-content {
147 position: relative;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000148 padding: 6px 10px;
149 border-radius: 12px;
150 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700151 max-width: 100%;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000152 width: fit-content;
153 min-width: min-content;
Philip Zeyligere31d2a92025-05-11 15:22:35 -0700154 overflow-wrap: break-word;
155 word-break: break-word;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000156 }
157
158 /* User message styling */
159 .user .message-content {
160 background-color: #2196f3;
161 color: white;
162 border-bottom-right-radius: 5px;
163 }
164
165 /* Agent message styling */
166 .agent .message-content,
167 .tool .message-content,
168 .error .message-content {
169 background-color: #f1f1f1;
170 color: black;
171 border-bottom-left-radius: 5px;
Sean McCullough86b56862025-04-18 13:04:03 -0700172 }
173
174 /* Copy button styles */
175 .message-text-container,
176 .tool-result-container {
177 position: relative;
178 }
179
180 .message-actions {
181 position: absolute;
182 top: 5px;
183 right: 5px;
184 z-index: 10;
185 opacity: 0;
186 transition: opacity 0.2s ease;
187 }
188
189 .message-text-container:hover .message-actions,
190 .tool-result-container:hover .message-actions {
191 opacity: 1;
192 }
193
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000194 .message-actions {
Sean McCullough86b56862025-04-18 13:04:03 -0700195 display: flex;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000196 gap: 6px;
197 }
198
199 .copy-icon,
200 .info-icon {
201 background-color: transparent;
202 border: none;
203 color: rgba(0, 0, 0, 0.6);
204 cursor: pointer;
205 padding: 3px;
206 border-radius: 50%;
207 display: flex;
208 align-items: center;
209 justify-content: center;
210 width: 24px;
211 height: 24px;
212 transition: all 0.15s ease;
213 }
214
215 .user .copy-icon,
216 .user .info-icon {
217 color: rgba(255, 255, 255, 0.8);
218 }
219
220 .copy-icon:hover,
221 .info-icon:hover {
222 background-color: rgba(0, 0, 0, 0.08);
223 }
224
225 .user .copy-icon:hover,
226 .user .info-icon:hover {
227 background-color: rgba(255, 255, 255, 0.15);
228 }
229
230 /* Message metadata styling */
231 .message-type {
232 font-weight: bold;
233 font-size: 11px;
Sean McCullough86b56862025-04-18 13:04:03 -0700234 }
235
236 .message-timestamp {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000237 display: block;
Sean McCullough86b56862025-04-18 13:04:03 -0700238 font-size: 10px;
239 color: #888;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000240 margin-top: 2px;
241 }
242
243 .message-duration {
244 display: block;
245 font-size: 10px;
246 color: #888;
247 margin-top: 2px;
Sean McCullough86b56862025-04-18 13:04:03 -0700248 }
249
250 .message-usage {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000251 display: block;
Sean McCullough86b56862025-04-18 13:04:03 -0700252 font-size: 10px;
253 color: #888;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000254 margin-top: 3px;
Sean McCullough86b56862025-04-18 13:04:03 -0700255 }
256
257 .conversation-id {
258 font-family: monospace;
259 font-size: 12px;
260 padding: 2px 4px;
Sean McCullough86b56862025-04-18 13:04:03 -0700261 margin-left: auto;
262 }
263
264 .parent-info {
265 font-size: 11px;
266 opacity: 0.8;
267 }
268
269 .subconversation {
270 border-left: 2px solid transparent;
271 padding-left: 5px;
272 margin-left: 20px;
273 transition: margin-left 0.3s ease;
274 }
275
276 .message-text {
277 overflow-x: auto;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000278 margin-bottom: 0;
279 font-family: sans-serif;
280 padding: 2px 0;
Sean McCullough86b56862025-04-18 13:04:03 -0700281 user-select: text;
282 cursor: text;
283 -webkit-user-select: text;
284 -moz-user-select: text;
285 -ms-user-select: text;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000286 font-size: 14px;
287 line-height: 1.35;
288 text-align: left;
289 }
290
291 /* Style for code blocks within messages */
292 .message-text pre,
293 .message-text code {
294 font-family: monospace;
295 background: rgba(0, 0, 0, 0.05);
296 border-radius: 4px;
297 padding: 2px 4px;
298 overflow-x: auto;
299 max-width: 100%;
300 white-space: pre-wrap; /* Allow wrapping for very long lines */
301 word-break: break-all; /* Break words at any character */
302 box-sizing: border-box; /* Include padding in width calculation */
303 }
304
Pokey Rulea10f1512025-05-15 13:53:26 +0000305 /* Code block container styles */
306 .code-block-container {
307 position: relative;
308 margin: 8px 0;
309 border-radius: 6px;
310 overflow: hidden;
311 background: rgba(0, 0, 0, 0.05);
312 }
313
314 .user .code-block-container {
315 background: rgba(255, 255, 255, 0.2);
316 }
317
318 .code-block-header {
319 display: flex;
320 justify-content: space-between;
321 align-items: center;
322 padding: 4px 8px;
323 background: rgba(0, 0, 0, 0.1);
324 font-size: 12px;
325 }
326
327 .user .code-block-header {
328 background: rgba(255, 255, 255, 0.2);
329 color: white;
330 }
331
332 .code-language {
333 font-family: monospace;
334 font-size: 11px;
335 font-weight: 500;
336 }
337
338 .code-copy-button {
339 background: transparent;
340 border: none;
341 color: inherit;
342 cursor: pointer;
343 padding: 2px;
344 border-radius: 3px;
345 display: flex;
346 align-items: center;
347 justify-content: center;
348 opacity: 0.7;
349 transition: all 0.15s ease;
350 }
351
352 .code-copy-button:hover {
353 opacity: 1;
354 background: rgba(0, 0, 0, 0.1);
355 }
356
357 .user .code-copy-button:hover {
358 background: rgba(255, 255, 255, 0.2);
359 }
360
361 .code-block-container pre {
362 margin: 0;
363 padding: 8px;
364 background: transparent;
365 }
366
367 .code-block-container code {
368 background: transparent;
369 padding: 0;
370 display: block;
371 width: 100%;
372 }
373
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000374 .user .message-text pre,
375 .user .message-text code {
376 background: rgba(255, 255, 255, 0.2);
377 color: white;
Sean McCullough86b56862025-04-18 13:04:03 -0700378 }
379
380 .tool-details {
381 margin-top: 3px;
382 padding-top: 3px;
383 border-top: 1px dashed #e0e0e0;
384 font-size: 12px;
385 }
386
387 .tool-name {
388 font-size: 12px;
389 font-weight: bold;
390 margin-bottom: 2px;
391 background: #f0f0f0;
392 padding: 2px 4px;
393 border-radius: 2px;
394 display: flex;
395 align-items: center;
396 gap: 3px;
397 }
398
399 .tool-input,
400 .tool-result {
401 margin-top: 2px;
402 padding: 3px 5px;
403 background: #f7f7f7;
404 border-radius: 2px;
405 font-family: monospace;
406 font-size: 12px;
407 overflow-x: auto;
408 white-space: pre;
409 line-height: 1.3;
410 user-select: text;
411 cursor: text;
412 -webkit-user-select: text;
413 -moz-user-select: text;
414 -ms-user-select: text;
415 }
416
417 .tool-result {
418 max-height: 300px;
419 overflow-y: auto;
420 }
421
422 .usage-info {
423 margin-top: 10px;
424 padding-top: 10px;
425 border-top: 1px dashed #e0e0e0;
426 font-size: 12px;
427 color: #666;
428 }
429
430 /* Custom styles for IRC-like experience */
431 .user .message-content {
432 border-left-color: #2196f3;
433 }
434
435 .agent .message-content {
436 border-left-color: #4caf50;
437 }
438
439 .tool .message-content {
440 border-left-color: #ff9800;
441 }
442
443 .error .message-content {
444 border-left-color: #f44336;
445 }
446
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700447 /* Compact message styling - distinct visual separation */
448 .compact {
449 background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
450 border: 2px solid #fd7e14;
451 border-radius: 12px;
452 margin: 20px 0;
453 padding: 0;
454 }
455
456 .compact .message-content {
457 border-left: 4px solid #fd7e14;
458 background: rgba(253, 126, 20, 0.05);
459 font-weight: 500;
460 }
461
462 .compact .message-text {
463 color: #8b4513;
464 font-size: 13px;
465 line-height: 1.4;
466 }
467
468 .compact::before {
469 content: "📚 CONVERSATION EPOCH";
470 display: block;
471 text-align: center;
472 font-size: 11px;
473 font-weight: bold;
474 color: #8b4513;
475 background: #fd7e14;
476 color: white;
477 padding: 4px 8px;
478 margin: 0;
479 border-radius: 8px 8px 0 0;
480 letter-spacing: 1px;
481 }
482
483 /* Pre-compaction messages get a subtle diagonal stripe background */
484 .pre-compaction {
485 background: repeating-linear-gradient(
486 45deg,
487 #ffffff,
488 #ffffff 10px,
489 #f8f8f8 10px,
490 #f8f8f8 20px
491 );
492 opacity: 0.85;
493 border-left: 3px solid #ddd;
494 }
495
496 .pre-compaction .message-content {
497 background: rgba(255, 255, 255, 0.7);
498 backdrop-filter: blur(1px);
Philip Zeyliger57d28bc2025-06-06 20:28:34 +0000499 color: #333; /* Ensure dark text for readability */
500 }
501
502 .pre-compaction .message-text {
503 color: #333; /* Ensure dark text in message content */
Philip Zeyligerb8a8f352025-06-02 07:39:37 -0700504 }
505
Sean McCullough86b56862025-04-18 13:04:03 -0700506 /* Make message type display bold but without the IRC-style markers */
507 .message-type {
508 font-weight: bold;
509 }
510
511 /* Commit message styling */
Sean McCullough86b56862025-04-18 13:04:03 -0700512 .commits-container {
513 margin-top: 10px;
Sean McCullough86b56862025-04-18 13:04:03 -0700514 }
515
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000516 .commit-notification {
517 background-color: #e8f5e9;
518 color: #2e7d32;
519 font-weight: 500;
520 font-size: 12px;
521 padding: 6px 10px;
522 border-radius: 10px;
523 margin-bottom: 8px;
524 text-align: center;
525 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
Sean McCullough86b56862025-04-18 13:04:03 -0700526 }
527
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000528 .commit-card {
529 background-color: #f5f5f5;
530 border-radius: 8px;
Sean McCullough86b56862025-04-18 13:04:03 -0700531 overflow: hidden;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000532 margin-bottom: 6px;
533 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
534 padding: 6px 8px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000535 display: flex;
536 align-items: center;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000537 gap: 8px;
Sean McCullough86b56862025-04-18 13:04:03 -0700538 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700539
Sean McCullough86b56862025-04-18 13:04:03 -0700540 .commit-hash {
541 color: #0366d6;
542 font-weight: bold;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000543 font-family: monospace;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000544 cursor: pointer;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000545 text-decoration: none;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000546 background-color: rgba(3, 102, 214, 0.08);
547 padding: 2px 5px;
548 border-radius: 4px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000549 }
550
551 .commit-hash:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000552 background-color: rgba(3, 102, 214, 0.15);
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000553 }
554
555 .commit-branch {
556 color: #28a745;
557 font-weight: 500;
558 cursor: pointer;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000559 font-family: monospace;
560 background-color: rgba(40, 167, 69, 0.08);
561 padding: 2px 5px;
562 border-radius: 4px;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000563 }
564
565 .commit-branch:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000566 background-color: rgba(40, 167, 69, 0.15);
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000567 }
568
philip.zeyliger6d3de482025-06-10 19:38:14 -0700569 .commit-branch-container {
570 display: flex;
571 align-items: center;
572 gap: 6px;
573 }
574
575 .commit-branch-container .copy-icon {
576 opacity: 0.7;
577 display: flex;
578 align-items: center;
579 }
580
581 .commit-branch-container .copy-icon svg {
582 vertical-align: middle;
583 }
584
585 .commit-branch-container:hover .copy-icon {
586 opacity: 1;
587 }
588
589 .octocat-link {
590 color: #586069;
591 text-decoration: none;
592 display: flex;
593 align-items: center;
594 transition: color 0.2s ease;
595 }
596
597 .octocat-link:hover {
598 color: #0366d6;
599 }
600
601 .octocat-icon {
602 width: 14px;
603 height: 14px;
604 }
605
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000606 .commit-subject {
607 font-size: 13px;
608 color: #333;
609 flex-grow: 1;
610 overflow: hidden;
611 text-overflow: ellipsis;
612 white-space: nowrap;
Sean McCullough86b56862025-04-18 13:04:03 -0700613 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700614
Sean McCullough86b56862025-04-18 13:04:03 -0700615 .commit-diff-button {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000616 padding: 3px 8px;
617 border: none;
618 border-radius: 4px;
619 background-color: #0366d6;
620 color: white;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000621 font-size: 11px;
Sean McCullough86b56862025-04-18 13:04:03 -0700622 cursor: pointer;
623 transition: all 0.2s ease;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000624 display: block;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +0000625 margin-left: auto;
Sean McCullough86b56862025-04-18 13:04:03 -0700626 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700627
Sean McCullough86b56862025-04-18 13:04:03 -0700628 .commit-diff-button:hover {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000629 background-color: #0256b4;
Sean McCullough86b56862025-04-18 13:04:03 -0700630 }
Sean McCullough71941bd2025-04-18 13:31:48 -0700631
Sean McCullough86b56862025-04-18 13:04:03 -0700632 /* Tool call cards */
633 .tool-call-cards-container {
634 display: flex;
635 flex-direction: column;
636 gap: 8px;
637 margin-top: 8px;
638 }
639
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000640 /* Error message specific styling */
641 .error .message-content {
642 background-color: #ffebee;
643 border-left: 3px solid #f44336;
Sean McCullough86b56862025-04-18 13:04:03 -0700644 }
645
646 .end-of-turn {
647 margin-bottom: 15px;
648 }
649
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000650 .end-of-turn-indicator {
651 display: block;
652 font-size: 11px;
653 color: #777;
654 padding: 2px 0;
655 margin-top: 8px;
656 text-align: right;
657 font-style: italic;
658 }
659
660 .user .end-of-turn-indicator {
661 color: rgba(255, 255, 255, 0.7);
662 }
663
664 /* Message info panel styling */
665 .message-info-panel {
666 margin-top: 8px;
667 padding: 8px;
668 background-color: rgba(0, 0, 0, 0.03);
669 border-radius: 6px;
670 font-size: 12px;
671 transition: all 0.2s ease;
672 border-left: 2px solid rgba(0, 0, 0, 0.1);
673 }
674
bankseancad67b02025-06-27 21:57:05 +0000675 /* User name styling - positioned outside and below the message bubble */
676 .user-name-container {
677 display: flex;
678 justify-content: flex-end;
679 margin-top: 4px;
680 padding-right: 80px; /* Account for right metadata area */
681 }
682
683 :host([compactpadding]) .user-name-container {
684 padding-right: 0; /* No right padding in compact mode */
685 }
686
687 .user-name {
688 font-size: 11px;
689 color: #666;
690 font-style: italic;
691 text-align: right;
692 }
693
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000694 .user .message-info-panel {
695 background-color: rgba(255, 255, 255, 0.15);
696 border-left: 2px solid rgba(255, 255, 255, 0.2);
697 }
698
699 .info-row {
700 margin-bottom: 3px;
701 display: flex;
702 }
703
704 .info-label {
705 font-weight: bold;
706 margin-right: 5px;
707 min-width: 60px;
708 }
709
710 .info-value {
711 flex: 1;
712 }
713
714 .conversation-id {
715 font-family: monospace;
Sean McCullough86b56862025-04-18 13:04:03 -0700716 font-size: 10px;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000717 word-break: break-all;
Sean McCullough86b56862025-04-18 13:04:03 -0700718 }
719
720 .markdown-content {
721 box-sizing: border-box;
722 min-width: 200px;
723 margin: 0 auto;
724 }
725
726 .markdown-content p {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +0000727 margin-block-start: 0.3em;
728 margin-block-end: 0.3em;
729 }
730
731 .markdown-content p:first-child {
732 margin-block-start: 0;
733 }
734
735 .markdown-content p:last-child {
736 margin-block-end: 0;
737 }
738
739 /* Styling for markdown elements */
740 .markdown-content a {
741 color: inherit;
742 text-decoration: underline;
743 }
744
745 .user .markdown-content a {
746 color: #fff;
747 text-decoration: underline;
748 }
749
750 .markdown-content ul,
751 .markdown-content ol {
752 padding-left: 1.5em;
753 margin: 0.5em 0;
754 }
755
756 .markdown-content blockquote {
757 border-left: 3px solid rgba(0, 0, 0, 0.2);
758 padding-left: 1em;
759 margin-left: 0.5em;
760 font-style: italic;
761 }
762
763 .user .markdown-content blockquote {
764 border-left: 3px solid rgba(255, 255, 255, 0.4);
Sean McCullough86b56862025-04-18 13:04:03 -0700765 }
Autoformatterdded2d62025-04-28 00:27:21 +0000766
Sean McCullough8d93e362025-04-27 23:32:18 +0000767 /* Mermaid diagram styling */
768 .mermaid-container {
769 margin: 1em 0;
770 padding: 0.5em;
771 background-color: #f8f8f8;
772 border-radius: 4px;
773 overflow-x: auto;
774 }
Autoformatterdded2d62025-04-28 00:27:21 +0000775
Sean McCullough8d93e362025-04-27 23:32:18 +0000776 .mermaid {
777 text-align: center;
778 }
philip.zeyligerffa94c62025-06-19 18:43:37 -0700779
780 /* Print styles for message components */
781 @media print {
782 .message {
783 page-break-inside: avoid;
784 margin-bottom: 12px;
785 }
786
787 .message-container {
788 page-break-inside: avoid;
789 }
790
791 /* Hide copy buttons and interactive elements during printing */
792 .copy-icon,
793 .info-icon,
794 .commit-diff-button {
795 display: none !important;
796 }
797
798 /* Ensure code blocks print properly */
799 .message-content pre {
800 white-space: pre-wrap;
801 word-wrap: break-word;
802 page-break-inside: avoid;
803 background: #f8f8f8 !important;
804 border: 1px solid #ddd !important;
805 padding: 8px !important;
806 }
807
808 /* Ensure tool calls section prints properly */
809 .tool-calls-section {
810 page-break-inside: avoid;
811 }
812
813 /* Simplify message metadata for print */
814 .message-metadata-left {
815 font-size: 10px;
816 }
817
818 /* Ensure content doesn't break poorly */
819 .message-content {
820 orphans: 3;
821 widows: 3;
822 }
823
824 /* Hide floating messages during print */
825 .floating-message {
826 display: none !important;
827 }
828 }
Sean McCullough86b56862025-04-18 13:04:03 -0700829 `;
830
Sean McCullough8d93e362025-04-27 23:32:18 +0000831 // Track mermaid diagrams that need rendering
832 private mermaidDiagrams = new Map();
833
Sean McCullough86b56862025-04-18 13:04:03 -0700834 constructor() {
835 super();
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000836 // Mermaid will be initialized lazily when first needed
Sean McCullough86b56862025-04-18 13:04:03 -0700837 }
838
839 // See https://lit.dev/docs/components/lifecycle/
840 connectedCallback() {
841 super.connectedCallback();
842 }
Autoformatterdded2d62025-04-28 00:27:21 +0000843
Sean McCullough8d93e362025-04-27 23:32:18 +0000844 // After the component is updated and rendered, render any mermaid diagrams
845 updated(changedProperties: Map<string, unknown>) {
846 super.updated(changedProperties);
847 this.renderMermaidDiagrams();
Pokey Rulea10f1512025-05-15 13:53:26 +0000848 this.setupCodeBlockCopyButtons();
Sean McCullough8d93e362025-04-27 23:32:18 +0000849 }
Autoformatterdded2d62025-04-28 00:27:21 +0000850
Sean McCullough8d93e362025-04-27 23:32:18 +0000851 // Render mermaid diagrams after the component is updated
852 renderMermaidDiagrams() {
853 // Add a small delay to ensure the DOM is fully rendered
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000854 setTimeout(async () => {
Sean McCullough8d93e362025-04-27 23:32:18 +0000855 // Find all mermaid containers in our shadow root
Autoformatterdded2d62025-04-28 00:27:21 +0000856 const containers = this.shadowRoot?.querySelectorAll(".mermaid");
Sean McCullough8d93e362025-04-27 23:32:18 +0000857 if (!containers || containers.length === 0) return;
Autoformatterdded2d62025-04-28 00:27:21 +0000858
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000859 try {
860 // Load mermaid dynamically
861 const mermaidLib = await loadMermaid();
Autoformatterdded2d62025-04-28 00:27:21 +0000862
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000863 // Initialize mermaid with specific config (only once per load)
864 mermaidLib.initialize({
865 startOnLoad: false,
866 suppressErrorRendering: true,
867 theme: "default",
868 securityLevel: "loose", // Allows more flexibility but be careful with user-generated content
869 fontFamily: "monospace",
870 });
Autoformatterdded2d62025-04-28 00:27:21 +0000871
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000872 // Process each mermaid diagram
873 containers.forEach((container) => {
874 const id = container.id;
875 const code = container.textContent || "";
876 if (!code || !id) return; // Use return for forEach instead of continue
877
878 try {
879 // Clear any previous content
880 container.innerHTML = code;
881
882 // Render the mermaid diagram using promise
883 mermaidLib
884 .render(`${id}-svg`, code)
885 .then(({ svg }) => {
886 container.innerHTML = svg;
887 })
888 .catch((err) => {
889 console.error("Error rendering mermaid diagram:", err);
890 // Show the original code as fallback
891 container.innerHTML = `<pre>${code}</pre>`;
892 });
893 } catch (err) {
894 console.error("Error processing mermaid diagram:", err);
895 // Show the original code as fallback
896 container.innerHTML = `<pre>${code}</pre>`;
897 }
898 });
899 } catch (err) {
900 console.error("Error loading mermaid:", err);
901 // Show the original code as fallback for all diagrams
902 containers.forEach((container) => {
903 const code = container.textContent || "";
Sean McCullough8d93e362025-04-27 23:32:18 +0000904 container.innerHTML = `<pre>${code}</pre>`;
philip.zeyliger7c1a6872025-06-16 03:54:37 +0000905 });
906 }
Sean McCullough8d93e362025-04-27 23:32:18 +0000907 }, 100); // Small delay to ensure DOM is ready
908 }
Sean McCullough86b56862025-04-18 13:04:03 -0700909
Pokey Rulea10f1512025-05-15 13:53:26 +0000910 // Setup code block copy buttons after component is updated
911 setupCodeBlockCopyButtons() {
912 setTimeout(() => {
913 // Find all copy buttons in code blocks
914 const copyButtons =
915 this.shadowRoot?.querySelectorAll(".code-copy-button");
916 if (!copyButtons || copyButtons.length === 0) return;
917
918 // Add click event listener to each button
919 copyButtons.forEach((button) => {
920 button.addEventListener("click", (e) => {
921 e.stopPropagation();
922 const codeId = (button as HTMLElement).dataset.codeId;
923 if (!codeId) return;
924
925 const codeElement = this.shadowRoot?.querySelector(`#${codeId}`);
926 if (!codeElement) return;
927
928 const codeText = codeElement.textContent || "";
929 const buttonRect = button.getBoundingClientRect();
930
931 // Copy code to clipboard
932 navigator.clipboard
933 .writeText(codeText)
934 .then(() => {
935 // Show success indicator
936 const originalHTML = button.innerHTML;
937 button.innerHTML = `
938 <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">
939 <path d="M20 6L9 17l-5-5"></path>
940 </svg>
941 `;
942
943 // Display floating message
944 this.showFloatingMessage("Copied!", buttonRect, "success");
945
946 // Reset button after delay
947 setTimeout(() => {
948 button.innerHTML = originalHTML;
949 }, 2000);
950 })
951 .catch((err) => {
952 console.error("Failed to copy code:", err);
953 this.showFloatingMessage("Failed to copy!", buttonRect, "error");
954 });
955 });
956 });
957 }, 100); // Small delay to ensure DOM is ready
958 }
959
Sean McCullough86b56862025-04-18 13:04:03 -0700960 // See https://lit.dev/docs/components/lifecycle/
961 disconnectedCallback() {
962 super.disconnectedCallback();
963 }
964
965 renderMarkdown(markdownContent: string): string {
966 try {
Sean McCullough8d93e362025-04-27 23:32:18 +0000967 // Create a custom renderer
968 const renderer = new Renderer();
969 const originalCodeRenderer = renderer.code.bind(renderer);
Autoformatterdded2d62025-04-28 00:27:21 +0000970
Pokey Rulea10f1512025-05-15 13:53:26 +0000971 // Override the code renderer to handle mermaid diagrams and add copy buttons
Autoformatterdded2d62025-04-28 00:27:21 +0000972 renderer.code = function ({ text, lang, escaped }: Tokens.Code): string {
973 if (lang === "mermaid") {
Sean McCullough8d93e362025-04-27 23:32:18 +0000974 // Generate a unique ID for this diagram
975 const id = `mermaid-diagram-${Math.random().toString(36).substring(2, 10)}`;
Autoformatterdded2d62025-04-28 00:27:21 +0000976
Sean McCullough8d93e362025-04-27 23:32:18 +0000977 // Just create the container and mermaid div - we'll render it in the updated() lifecycle method
978 return `<div class="mermaid-container">
979 <div class="mermaid" id="${id}">${text}</div>
980 </div>`;
981 }
Pokey Rulea10f1512025-05-15 13:53:26 +0000982
Philip Zeyliger0d092842025-06-09 18:57:12 -0700983 // For regular code blocks, call the original renderer to get properly escaped HTML
984 const originalCodeHtml = originalCodeRenderer({ text, lang, escaped });
985
986 // Extract the code content from the original HTML to add our custom wrapper
987 // The original renderer returns: <pre><code class="language-x">escapedText</code></pre>
988 const codeMatch = originalCodeHtml.match(
989 /<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/,
990 );
991 if (!codeMatch) {
992 // Fallback to original if we can't parse it
993 return originalCodeHtml;
994 }
995
996 const escapedText = codeMatch[1];
Pokey Rulea10f1512025-05-15 13:53:26 +0000997 const id = `code-block-${Math.random().toString(36).substring(2, 10)}`;
998 const langClass = lang ? ` class="language-${lang}"` : "";
999
1000 return `<div class="code-block-container">
1001 <div class="code-block-header">
1002 ${lang ? `<span class="code-language">${lang}</span>` : ""}
1003 <button class="code-copy-button" title="Copy code" data-code-id="${id}">
1004 <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">
1005 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1006 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1007 </svg>
1008 </button>
1009 </div>
Philip Zeyliger0d092842025-06-09 18:57:12 -07001010 <pre><code id="${id}"${langClass}>${escapedText}</code></pre>
Pokey Rulea10f1512025-05-15 13:53:26 +00001011 </div>`;
Sean McCullough8d93e362025-04-27 23:32:18 +00001012 };
Autoformatterdded2d62025-04-28 00:27:21 +00001013
Philip Zeyliger53ab2452025-06-04 17:49:33 +00001014 // Set markdown options for proper code block highlighting
Sean McCullough86b56862025-04-18 13:04:03 -07001015 const markedOptions: MarkedOptions = {
1016 gfm: true, // GitHub Flavored Markdown
1017 breaks: true, // Convert newlines to <br>
1018 async: false,
Autoformatterdded2d62025-04-28 00:27:21 +00001019 renderer: renderer,
Sean McCullough86b56862025-04-18 13:04:03 -07001020 };
Philip Zeyliger53ab2452025-06-04 17:49:33 +00001021
1022 // Parse markdown and sanitize the output HTML with DOMPurify
1023 const htmlOutput = marked.parse(markdownContent, markedOptions) as string;
1024 return DOMPurify.sanitize(htmlOutput, {
1025 // Allow common HTML elements that are safe
1026 ALLOWED_TAGS: [
1027 "p",
1028 "br",
1029 "strong",
1030 "em",
1031 "b",
1032 "i",
1033 "u",
1034 "s",
1035 "code",
1036 "pre",
1037 "h1",
1038 "h2",
1039 "h3",
1040 "h4",
1041 "h5",
1042 "h6",
1043 "ul",
1044 "ol",
1045 "li",
1046 "blockquote",
1047 "a",
1048 "div",
1049 "span", // For mermaid diagrams and code blocks
1050 "svg",
1051 "g",
1052 "path",
1053 "rect",
1054 "circle",
1055 "text",
1056 "line",
1057 "polygon", // For mermaid SVG
1058 "button", // For code copy buttons
1059 ],
1060 ALLOWED_ATTR: [
1061 "href",
1062 "title",
1063 "target",
1064 "rel", // For links
1065 "class",
1066 "id", // For styling and functionality
1067 "data-*", // For code copy buttons
1068 // SVG attributes for mermaid diagrams
1069 "viewBox",
1070 "width",
1071 "height",
1072 "xmlns",
1073 "fill",
1074 "stroke",
1075 "stroke-width",
1076 "d",
1077 "x",
1078 "y",
1079 "x1",
1080 "y1",
1081 "x2",
1082 "y2",
1083 "cx",
1084 "cy",
1085 "r",
1086 "rx",
1087 "ry",
1088 "points",
1089 "transform",
1090 "text-anchor",
1091 "font-size",
1092 "font-family",
1093 ],
1094 // Allow data attributes for functionality
1095 ALLOW_DATA_ATTR: true,
1096 // Keep whitespace for code formatting
1097 KEEP_CONTENT: true,
1098 });
Sean McCullough86b56862025-04-18 13:04:03 -07001099 } catch (error) {
1100 console.error("Error rendering markdown:", error);
Philip Zeyliger53ab2452025-06-04 17:49:33 +00001101 // Fallback to sanitized plain text if markdown parsing fails
1102 return DOMPurify.sanitize(markdownContent);
Sean McCullough86b56862025-04-18 13:04:03 -07001103 }
1104 }
1105
1106 /**
1107 * Format timestamp for display
1108 */
1109 formatTimestamp(
1110 timestamp: string | number | Date | null | undefined,
1111 defaultValue: string = "",
1112 ): string {
1113 if (!timestamp) return defaultValue;
1114 try {
1115 const date = new Date(timestamp);
1116 if (isNaN(date.getTime())) return defaultValue;
1117
1118 // Format: Mar 13, 2025 09:53:25 AM
1119 return date.toLocaleString("en-US", {
1120 month: "short",
1121 day: "numeric",
1122 year: "numeric",
1123 hour: "numeric",
1124 minute: "2-digit",
1125 second: "2-digit",
1126 hour12: true,
1127 });
philip.zeyliger26bc6592025-06-30 20:15:30 -07001128 } catch {
Sean McCullough86b56862025-04-18 13:04:03 -07001129 return defaultValue;
1130 }
1131 }
1132
1133 formatNumber(
1134 num: number | null | undefined,
1135 defaultValue: string = "0",
1136 ): string {
1137 if (num === undefined || num === null) return defaultValue;
1138 try {
1139 return num.toLocaleString();
philip.zeyliger26bc6592025-06-30 20:15:30 -07001140 } catch {
Sean McCullough86b56862025-04-18 13:04:03 -07001141 return String(num);
1142 }
1143 }
1144 formatCurrency(
1145 num: number | string | null | undefined,
1146 defaultValue: string = "$0.00",
1147 isMessageLevel: boolean = false,
1148 ): string {
1149 if (num === undefined || num === null) return defaultValue;
1150 try {
1151 // Use 4 decimal places for message-level costs, 2 for totals
1152 const decimalPlaces = isMessageLevel ? 4 : 2;
1153 return `$${parseFloat(String(num)).toFixed(decimalPlaces)}`;
philip.zeyliger26bc6592025-06-30 20:15:30 -07001154 } catch {
Sean McCullough86b56862025-04-18 13:04:03 -07001155 return defaultValue;
1156 }
1157 }
1158
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001159 // Format duration from nanoseconds to a human-readable string
1160 _formatDuration(nanoseconds: number | null | undefined): string {
1161 if (!nanoseconds) return "0s";
1162
1163 const seconds = nanoseconds / 1e9;
1164
1165 if (seconds < 60) {
1166 return `${seconds.toFixed(1)}s`;
1167 } else if (seconds < 3600) {
1168 const minutes = Math.floor(seconds / 60);
1169 const remainingSeconds = seconds % 60;
1170 return `${minutes}min ${remainingSeconds.toFixed(0)}s`;
1171 } else {
1172 const hours = Math.floor(seconds / 3600);
1173 const remainingSeconds = seconds % 3600;
1174 const minutes = Math.floor(remainingSeconds / 60);
1175 return `${hours}h ${minutes}min`;
1176 }
1177 }
1178
Sean McCullough86b56862025-04-18 13:04:03 -07001179 showCommit(commitHash: string) {
Sean McCullough71941bd2025-04-18 13:31:48 -07001180 this.dispatchEvent(
1181 new CustomEvent("show-commit-diff", {
1182 bubbles: true,
1183 composed: true,
1184 detail: { commitHash },
1185 }),
1186 );
Sean McCullough86b56862025-04-18 13:04:03 -07001187 }
1188
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001189 _toggleInfo(e: Event) {
1190 e.stopPropagation();
1191 this.showInfo = !this.showInfo;
1192 }
1193
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001194 copyToClipboard(text: string, event: Event) {
1195 const element = event.currentTarget as HTMLElement;
1196 const rect = element.getBoundingClientRect();
1197
1198 navigator.clipboard
1199 .writeText(text)
1200 .then(() => {
1201 this.showFloatingMessage("Copied!", rect, "success");
1202 })
1203 .catch((err) => {
1204 console.error("Failed to copy text: ", err);
1205 this.showFloatingMessage("Failed to copy!", rect, "error");
1206 });
1207 }
1208
1209 showFloatingMessage(
1210 message: string,
1211 targetRect: DOMRect,
1212 type: "success" | "error",
1213 ) {
1214 // Create floating message element
1215 const floatingMsg = document.createElement("div");
1216 floatingMsg.textContent = message;
1217 floatingMsg.className = `floating-message ${type}`;
1218
1219 // Position it near the clicked element
1220 // Position just above the element
1221 const top = targetRect.top - 30;
1222 const left = targetRect.left + targetRect.width / 2 - 40;
1223
1224 floatingMsg.style.position = "fixed";
1225 floatingMsg.style.top = `${top}px`;
1226 floatingMsg.style.left = `${left}px`;
1227 floatingMsg.style.zIndex = "9999";
1228
1229 // Add to document body
1230 document.body.appendChild(floatingMsg);
1231
1232 // Animate in
1233 floatingMsg.style.opacity = "0";
1234 floatingMsg.style.transform = "translateY(10px)";
1235
1236 setTimeout(() => {
1237 floatingMsg.style.opacity = "1";
1238 floatingMsg.style.transform = "translateY(0)";
1239 }, 10);
1240
1241 // Remove after animation
1242 setTimeout(() => {
1243 floatingMsg.style.opacity = "0";
1244 floatingMsg.style.transform = "translateY(-10px)";
1245
1246 setTimeout(() => {
1247 document.body.removeChild(floatingMsg);
1248 }, 300);
1249 }, 1500);
1250 }
1251
philip.zeyliger6d3de482025-06-10 19:38:14 -07001252 // Format GitHub repository URL to org/repo format
1253 formatGitHubRepo(url) {
1254 if (!url) return null;
1255
1256 // Common GitHub URL patterns
1257 const patterns = [
1258 // HTTPS URLs
1259 /https:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
1260 // SSH URLs
1261 /git@github\.com:([^/]+)\/([^/\s.]+)(?:\.git)?/,
1262 // Git protocol
1263 /git:\/\/github\.com\/([^/]+)\/([^/\s.]+)(?:\.git)?/,
1264 ];
1265
1266 for (const pattern of patterns) {
1267 const match = url.match(pattern);
1268 if (match) {
1269 return {
1270 formatted: `${match[1]}/${match[2]}`,
1271 url: `https://github.com/${match[1]}/${match[2]}`,
1272 owner: match[1],
1273 repo: match[2],
1274 };
1275 }
1276 }
1277
1278 return null;
1279 }
1280
1281 // Generate GitHub branch URL if linking is enabled
1282 getGitHubBranchLink(branchName) {
1283 if (!this.state?.link_to_github || !branchName) {
1284 return null;
1285 }
1286
1287 const github = this.formatGitHubRepo(this.state?.git_origin);
1288 if (!github) {
1289 return null;
1290 }
1291
1292 return `https://github.com/${github.owner}/${github.repo}/tree/${branchName}`;
1293 }
1294
Sean McCullough86b56862025-04-18 13:04:03 -07001295 render() {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001296 // Calculate if this is an end of turn message with no parent conversation ID
1297 const isEndOfTurn =
1298 this.message?.end_of_turn && !this.message?.parent_conversation_id;
1299
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001300 const isPreCompaction =
1301 this.message?.idx !== undefined &&
1302 this.message.idx < this.firstMessageIndex;
1303
Sean McCullough86b56862025-04-18 13:04:03 -07001304 return html`
1305 <div
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001306 class="message ${this.message?.type} ${isEndOfTurn
Sean McCullough86b56862025-04-18 13:04:03 -07001307 ? "end-of-turn"
Philip Zeyligerb8a8f352025-06-02 07:39:37 -07001308 : ""} ${isPreCompaction ? "pre-compaction" : ""}"
Sean McCullough86b56862025-04-18 13:04:03 -07001309 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001310 <div class="message-container">
1311 <!-- Left area (empty for simplicity) -->
1312 <div class="message-metadata-left"></div>
1313
1314 <!-- Message bubble -->
1315 <div class="message-bubble-container">
1316 <div class="message-content">
1317 <div class="message-text-container">
1318 <div class="message-actions">
1319 ${copyButton(this.message?.content)}
1320 <button
1321 class="info-icon"
1322 title="Show message details"
1323 @click=${this._toggleInfo}
Sean McCullough71941bd2025-04-18 13:31:48 -07001324 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001325 <svg
1326 xmlns="http://www.w3.org/2000/svg"
1327 width="16"
1328 height="16"
1329 viewBox="0 0 24 24"
1330 fill="none"
1331 stroke="currentColor"
1332 stroke-width="2"
1333 stroke-linecap="round"
1334 stroke-linejoin="round"
1335 >
1336 <circle cx="12" cy="12" r="10"></circle>
1337 <line x1="12" y1="16" x2="12" y2="12"></line>
1338 <line x1="12" y1="8" x2="12.01" y2="8"></line>
1339 </svg>
1340 </button>
1341 </div>
1342 ${this.message?.content
1343 ? html`
1344 <div class="message-text markdown-content">
1345 ${unsafeHTML(
1346 this.renderMarkdown(this.message?.content),
1347 )}
1348 </div>
1349 `
1350 : ""}
1351
1352 <!-- End of turn indicator inside the bubble -->
1353 ${isEndOfTurn && this.message?.elapsed
1354 ? html`
1355 <div class="end-of-turn-indicator">
1356 end of turn
1357 (${this._formatDuration(this.message?.elapsed)})
1358 </div>
1359 `
1360 : ""}
1361
1362 <!-- Info panel that can be toggled -->
1363 ${this.showInfo
1364 ? html`
1365 <div class="message-info-panel">
1366 <div class="info-row">
1367 <span class="info-label">Type:</span>
1368 <span class="info-value">${this.message?.type}</span>
1369 </div>
1370 <div class="info-row">
1371 <span class="info-label">Time:</span>
1372 <span class="info-value"
1373 >${this.formatTimestamp(
1374 this.message?.timestamp,
1375 "",
1376 )}</span
1377 >
1378 </div>
1379 ${this.message?.elapsed
1380 ? html`
1381 <div class="info-row">
1382 <span class="info-label">Duration:</span>
1383 <span class="info-value"
1384 >${this._formatDuration(
1385 this.message?.elapsed,
1386 )}</span
1387 >
1388 </div>
1389 `
1390 : ""}
1391 ${this.message?.usage
1392 ? html`
1393 <div class="info-row">
1394 <span class="info-label">Tokens:</span>
1395 <span class="info-value">
1396 ${this.message?.usage
1397 ? html`
1398 <div>
1399 Input:
1400 ${this.formatNumber(
1401 this.message?.usage?.input_tokens ||
1402 0,
1403 )}
1404 </div>
1405 ${this.message?.usage
1406 ?.cache_creation_input_tokens
1407 ? html`
1408 <div>
1409 Cache creation:
1410 ${this.formatNumber(
1411 this.message?.usage
1412 ?.cache_creation_input_tokens,
1413 )}
1414 </div>
1415 `
1416 : ""}
1417 ${this.message?.usage
1418 ?.cache_read_input_tokens
1419 ? html`
1420 <div>
1421 Cache read:
1422 ${this.formatNumber(
1423 this.message?.usage
1424 ?.cache_read_input_tokens,
1425 )}
1426 </div>
1427 `
1428 : ""}
1429 <div>
1430 Output:
1431 ${this.formatNumber(
1432 this.message?.usage?.output_tokens,
1433 )}
1434 </div>
1435 <div>
1436 Cost:
1437 ${this.formatCurrency(
1438 this.message?.usage?.cost_usd,
1439 )}
1440 </div>
1441 `
1442 : "N/A"}
1443 </span>
1444 </div>
1445 `
1446 : ""}
1447 ${this.message?.conversation_id
1448 ? html`
1449 <div class="info-row">
1450 <span class="info-label">Conversation ID:</span>
1451 <span class="info-value conversation-id"
1452 >${this.message?.conversation_id}</span
1453 >
1454 </div>
1455 `
1456 : ""}
1457 </div>
1458 `
1459 : ""}
1460 </div>
1461
1462 <!-- Tool calls - only shown for agent messages -->
1463 ${this.message?.type === "agent"
1464 ? html`
1465 <sketch-tool-calls
1466 .toolCalls=${this.message?.tool_calls}
1467 .open=${this.open}
1468 ></sketch-tool-calls>
1469 `
1470 : ""}
1471
1472 <!-- Commits section (redesigned as bubbles) -->
1473 ${this.message?.commits
1474 ? html`
1475 <div class="commits-container">
1476 <div class="commit-notification">
1477 ${this.message.commits.length} new
1478 commit${this.message.commits.length > 1 ? "s" : ""}
1479 detected
1480 </div>
1481 ${this.message.commits.map((commit) => {
1482 return html`
1483 <div class="commit-card">
Philip Zeyliger72682df2025-04-23 13:09:46 -07001484 <span
1485 class="commit-hash"
1486 title="Click to copy: ${commit.hash}"
1487 @click=${(e) =>
1488 this.copyToClipboard(
1489 commit.hash.substring(0, 8),
1490 e,
1491 )}
1492 >
Pokey Rule7be879f2025-04-23 15:30:15 +01001493 ${commit.hash.substring(0, 8)}
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001494 </span>
1495 ${commit.pushed_branch
philip.zeyliger6d3de482025-06-10 19:38:14 -07001496 ? (() => {
1497 const githubLink = this.getGitHubBranchLink(
1498 commit.pushed_branch,
1499 );
1500 return html`
1501 <div class="commit-branch-container">
1502 <span
1503 class="commit-branch pushed-branch"
1504 title="Click to copy: ${commit.pushed_branch}"
1505 @click=${(e) =>
1506 this.copyToClipboard(
1507 commit.pushed_branch,
1508 e,
1509 )}
1510 >${commit.pushed_branch}</span
1511 >
cbroebbdee42025-06-20 09:57:44 +00001512 <span
1513 class="copy-icon"
1514 @click=${(e) => {
1515 e.stopPropagation();
1516 this.copyToClipboard(
1517 commit.pushed_branch,
1518 e,
1519 );
1520 }}
1521 >
philip.zeyliger6d3de482025-06-10 19:38:14 -07001522 <svg
1523 xmlns="http://www.w3.org/2000/svg"
1524 width="14"
1525 height="14"
1526 viewBox="0 0 24 24"
1527 fill="none"
1528 stroke="currentColor"
1529 stroke-width="2"
1530 stroke-linecap="round"
1531 stroke-linejoin="round"
1532 >
1533 <rect
1534 x="9"
1535 y="9"
1536 width="13"
1537 height="13"
1538 rx="2"
1539 ry="2"
1540 ></rect>
1541 <path
1542 d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
1543 ></path>
1544 </svg>
1545 </span>
1546 ${githubLink
1547 ? html`
1548 <a
1549 href="${githubLink}"
1550 target="_blank"
1551 rel="noopener noreferrer"
1552 class="octocat-link"
1553 title="Open ${commit.pushed_branch} on GitHub"
1554 @click=${(e) =>
1555 e.stopPropagation()}
1556 >
1557 <svg
1558 class="octocat-icon"
1559 viewBox="0 0 16 16"
1560 width="14"
1561 height="14"
1562 >
1563 <path
1564 fill="currentColor"
1565 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"
1566 />
1567 </svg>
1568 </a>
1569 `
1570 : ""}
1571 </div>
1572 `;
1573 })()
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001574 : ``}
1575 <span class="commit-subject"
1576 >${commit.subject}</span
Sean McCullough71941bd2025-04-18 13:31:48 -07001577 >
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001578 <button
1579 class="commit-diff-button"
1580 @click=${() => this.showCommit(commit.hash)}
1581 >
1582 View Diff
1583 </button>
Sean McCullough86b56862025-04-18 13:04:03 -07001584 </div>
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001585 `;
1586 })}
1587 </div>
1588 `
1589 : ""}
1590 </div>
1591 </div>
1592
1593 <!-- Right side (empty for consistency) -->
1594 <div class="message-metadata-right"></div>
Sean McCullough86b56862025-04-18 13:04:03 -07001595 </div>
bankseancad67b02025-06-27 21:57:05 +00001596
1597 <!-- User name for user messages - positioned outside and below the bubble -->
1598 ${this.message?.type === "user" && this.state?.git_username
1599 ? html`
1600 <div class="user-name-container">
1601 <div class="user-name">${this.state.git_username}</div>
1602 </div>
1603 `
1604 : ""}
Sean McCullough86b56862025-04-18 13:04:03 -07001605 </div>
1606 `;
1607 }
1608}
1609
Sean McCullough71941bd2025-04-18 13:31:48 -07001610function copyButton(textToCopy: string) {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001611 // Use an icon of overlapping rectangles for copy
1612 const buttonClass = "copy-icon";
1613
1614 // SVG for copy icon (two overlapping rectangles)
1615 const copyIcon = html`<svg
1616 xmlns="http://www.w3.org/2000/svg"
1617 width="16"
1618 height="16"
1619 viewBox="0 0 24 24"
1620 fill="none"
1621 stroke="currentColor"
1622 stroke-width="2"
1623 stroke-linecap="round"
1624 stroke-linejoin="round"
1625 >
1626 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1627 <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
1628 </svg>`;
1629
1630 // SVG for success check mark
1631 const successIcon = html`<svg
1632 xmlns="http://www.w3.org/2000/svg"
1633 width="16"
1634 height="16"
1635 viewBox="0 0 24 24"
1636 fill="none"
1637 stroke="currentColor"
1638 stroke-width="2"
1639 stroke-linecap="round"
1640 stroke-linejoin="round"
1641 >
1642 <path d="M20 6L9 17l-5-5"></path>
1643 </svg>`;
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001644
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001645 const ret = html`<button
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001646 class="${buttonClass}"
1647 title="Copy to clipboard"
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001648 @click=${(e: Event) => {
1649 e.stopPropagation();
1650 const copyButton = e.currentTarget as HTMLButtonElement;
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001651 const originalInnerHTML = copyButton.innerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001652 navigator.clipboard
1653 .writeText(textToCopy)
1654 .then(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001655 copyButton.innerHTML = "";
1656 const successElement = document.createElement("div");
1657 copyButton.appendChild(successElement);
1658 render(successIcon, successElement);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001659 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001660 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001661 }, 2000);
1662 })
1663 .catch((err) => {
1664 console.error("Failed to copy text: ", err);
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001665 setTimeout(() => {
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001666 copyButton.innerHTML = originalInnerHTML;
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001667 }, 2000);
1668 });
1669 }}
1670 >
Philip Zeyliger16fa8b42025-05-02 04:28:16 +00001671 ${copyIcon}
Sean McCulloughec3ad1a2025-04-18 13:55:16 -07001672 </button>`;
Sean McCullough86b56862025-04-18 13:04:03 -07001673
Sean McCullough71941bd2025-04-18 13:31:48 -07001674 return ret;
Sean McCullough86b56862025-04-18 13:04:03 -07001675}
1676
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001677// Create global styles for floating messages
1678const floatingMessageStyles = document.createElement("style");
1679floatingMessageStyles.textContent = `
1680 .floating-message {
1681 background-color: rgba(0, 0, 0, 0.8);
1682 color: white;
1683 padding: 5px 10px;
1684 border-radius: 4px;
1685 font-size: 12px;
1686 font-family: system-ui, sans-serif;
1687 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
1688 pointer-events: none;
1689 transition: opacity 0.3s ease, transform 0.3s ease;
1690 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001691
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001692 .floating-message.success {
1693 background-color: rgba(40, 167, 69, 0.9);
1694 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +00001695
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001696 .floating-message.error {
1697 background-color: rgba(220, 53, 69, 0.9);
1698 }
Philip Zeyligere31d2a92025-05-11 15:22:35 -07001699
1700 /* Style for code, pre elements, and tool components to ensure proper wrapping/truncation */
1701 pre, code, sketch-tool-calls, sketch-tool-card, sketch-tool-card-bash {
1702 white-space: nowrap;
1703 overflow: hidden;
1704 text-overflow: ellipsis;
1705 max-width: 100%;
1706 }
1707
1708 /* Special rule for the message content container */
1709 .message-content {
1710 max-width: 100% !important;
1711 overflow: hidden !important;
1712 }
1713
1714 /* Ensure tool call containers don't overflow */
1715 ::slotted(sketch-tool-calls) {
1716 max-width: 100%;
1717 width: 100%;
1718 overflow-wrap: break-word;
1719 word-break: break-word;
1720 }
Philip Zeyliger37dc4cf2025-04-23 12:58:52 +00001721`;
1722document.head.appendChild(floatingMessageStyles);
1723
Sean McCullough86b56862025-04-18 13:04:03 -07001724declare global {
1725 interface HTMLElementTagNameMap {
1726 "sketch-timeline-message": SketchTimelineMessage;
1727 }
1728}