@@ -852,6 +852,37 @@ def is_tool_result_message(message_data):
852852 )
853853
854854
855+ def extract_markdown_content (message_data , log_type ):
856+ """Extract markdown/text content from a message for copying."""
857+ content = message_data .get ("content" , "" )
858+ if log_type == "user" :
859+ if isinstance (content , str ):
860+ if not is_json_like (content ):
861+ return content , "Copy Markdown" , None
862+ return None , "Copy JSON" , ".message-content pre"
863+ elif isinstance (content , list ):
864+ # Check for tool results (use selector) or text (use content)
865+ for block in content :
866+ if isinstance (block , dict ):
867+ if block .get ("type" ) == "tool_result" :
868+ return None , "Copy text" , ".message-content"
869+ if block .get ("type" ) == "text" :
870+ text = block .get ("text" , "" )
871+ if text and not is_json_like (text ):
872+ return text , "Copy Markdown" , None
873+ return None , "Copy text" , ".message-content"
874+ elif log_type == "assistant" :
875+ # For assistant messages, extract text blocks
876+ if isinstance (content , list ):
877+ texts = []
878+ for block in content :
879+ if isinstance (block , dict ) and block .get ("type" ) == "text" :
880+ texts .append (block .get ("text" , "" ))
881+ if texts :
882+ return "\n \n " .join (texts ), "Copy Markdown" , None
883+ return None , "Copy text" , ".message-content"
884+
885+
855886def render_message (log_type , message_json , timestamp ):
856887 if not message_json :
857888 return ""
@@ -874,7 +905,19 @@ def render_message(log_type, message_json, timestamp):
874905 if not content_html .strip ():
875906 return ""
876907 msg_id = make_msg_id (timestamp )
877- return _macros .message (role_class , role_label , msg_id , timestamp , content_html )
908+ copy_content , copy_label , copy_from = extract_markdown_content (
909+ message_data , log_type
910+ )
911+ return _macros .message (
912+ role_class ,
913+ role_label ,
914+ msg_id ,
915+ timestamp ,
916+ content_html ,
917+ copy_label = copy_label ,
918+ copy_content = copy_content ,
919+ copy_from = copy_from ,
920+ )
878921
879922
880923CSS = """
@@ -893,6 +936,7 @@ def render_message(log_type, message_json, timestamp):
893936.tool-reply .tool-result { background: transparent; padding: 0; margin: 0; }
894937.tool-reply .tool-result .truncatable.truncated::after { background: linear-gradient(to bottom, transparent, #fff8e1); }
895938.message-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; background: rgba(0,0,0,0.03); font-size: 0.85rem; }
939+ .header-actions { display: flex; align-items: center; gap: 8px; }
896940.role-label { font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
897941.user .role-label { color: var(--user-border); }
898942time { color: var(--text-muted); font-size: 0.8rem; }
@@ -977,6 +1021,7 @@ def render_message(log_type, message_json, timestamp):
9771021.index-item a { display: block; text-decoration: none; color: inherit; }
9781022.index-item a:hover { background: rgba(25, 118, 210, 0.1); }
9791023.index-item-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; background: rgba(0,0,0,0.03); font-size: 0.85rem; }
1024+ .index-item-header .header-actions { display: flex; align-items: center; gap: 8px; }
9801025.index-item-number { font-weight: 600; color: var(--user-border); }
9811026.index-item-content { padding: 16px; }
9821027.index-item-stats { padding: 8px 16px 12px 32px; font-size: 0.85rem; color: var(--text-muted); border-top: 1px solid rgba(0,0,0,0.06); }
@@ -1010,13 +1055,55 @@ def render_message(log_type, message_json, timestamp):
10101055.search-result { margin-bottom: 16px; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
10111056.search-result a { display: block; text-decoration: none; color: inherit; }
10121057.search-result a:hover { background: rgba(25, 118, 210, 0.05); }
1013- .search-result-page { padding: 6px 12px; background: rgba(0,0,0,0.03); font-size: 0.8rem; color: var(--text-muted); border-bottom: 1px solid rgba(0,0,0,0.06); }
1058+ .search-result-header { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: rgba(0,0,0,0.03); border-bottom: 1px solid rgba(0,0,0,0.06); }
1059+ .search-result-page { font-size: 0.8rem; color: var(--text-muted); text-decoration: none; }
1060+ .search-result-page:hover { text-decoration: underline; }
10141061.search-result-content { padding: 12px; }
10151062.search-result mark { background: #fff59d; padding: 1px 2px; border-radius: 2px; }
1063+ copy-button { display: inline-block; margin-right: 8px; }
1064+ copy-button button { background: transparent; border: 1px solid var(--text-muted); border-radius: 4px; padding: 2px 6px; font-size: 0.7rem; color: var(--text-muted); cursor: pointer; white-space: nowrap; }
1065+ copy-button button:hover { background: rgba(0,0,0,0.05); border-color: var(--user-border); color: var(--user-border); }
1066+ copy-button button.copied { background: #e8f5e9; border-color: #4caf50; color: #2e7d32; }
10161067@media (max-width: 600px) { body { padding: 8px; } .message, .index-item { border-radius: 8px; } .message-content, .index-item-content { padding: 12px; } pre { font-size: 0.8rem; padding: 8px; } #search-box input { width: 120px; } #search-modal[open] { width: 95vw; height: 90vh; } }
10171068"""
10181069
10191070JS = """
1071+ class CopyButton extends HTMLElement {
1072+ constructor() {
1073+ super();
1074+ this.attachShadow({ mode: 'open' });
1075+ }
1076+ connectedCallback() {
1077+ const label = this.getAttribute('label') || 'Copy';
1078+ const style = document.createElement('style');
1079+ style.textContent = 'button { background: transparent; border: 1px solid #757575; border-radius: 4px; padding: 2px 6px; font-size: 0.7rem; color: #757575; cursor: pointer; white-space: nowrap; font-family: inherit; } button:hover { background: rgba(0,0,0,0.05); border-color: #1976d2; color: #1976d2; } button.copied { background: #e8f5e9; border-color: #4caf50; color: #2e7d32; }';
1080+ const btn = document.createElement('button');
1081+ btn.textContent = label;
1082+ btn.addEventListener('click', (e) => this.handleClick(e, btn, label));
1083+ this.shadowRoot.appendChild(style);
1084+ this.shadowRoot.appendChild(btn);
1085+ }
1086+ handleClick(e, btn, label) {
1087+ e.preventDefault();
1088+ e.stopPropagation();
1089+ let content = this.getAttribute('data-content');
1090+ if (!content) {
1091+ const selector = this.getAttribute('data-content-from');
1092+ if (selector) {
1093+ const el = this.closest('.message, .index-item, .index-commit, .search-result')?.querySelector(selector) || document.querySelector(selector);
1094+ if (el) content = el.innerText;
1095+ }
1096+ }
1097+ if (content) {
1098+ navigator.clipboard.writeText(content).then(() => {
1099+ btn.textContent = 'Copied!';
1100+ btn.classList.add('copied');
1101+ setTimeout(() => { btn.textContent = label; btn.classList.remove('copied'); }, 2000);
1102+ });
1103+ }
1104+ }
1105+ }
1106+ customElements.define('copy-button', CopyButton);
10201107document.querySelectorAll('time[data-timestamp]').forEach(function(el) {
10211108 const timestamp = el.getAttribute('data-timestamp');
10221109 const date = new Date(timestamp);
@@ -1293,7 +1380,12 @@ def generate_html(json_path, output_dir, github_repo=None):
12931380 stats_html = _macros .index_stats (tool_stats_str , long_texts_html )
12941381
12951382 item_html = _macros .index_item (
1296- prompt_num , link , conv ["timestamp" ], rendered_content , stats_html
1383+ prompt_num ,
1384+ link ,
1385+ conv ["timestamp" ],
1386+ rendered_content ,
1387+ stats_html ,
1388+ copy_content = conv ["user_text" ],
12971389 )
12981390 timeline_items .append ((conv ["timestamp" ], "prompt" , item_html ))
12991391
@@ -1708,7 +1800,12 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
17081800 stats_html = _macros .index_stats (tool_stats_str , long_texts_html )
17091801
17101802 item_html = _macros .index_item (
1711- prompt_num , link , conv ["timestamp" ], rendered_content , stats_html
1803+ prompt_num ,
1804+ link ,
1805+ conv ["timestamp" ],
1806+ rendered_content ,
1807+ stats_html ,
1808+ copy_content = conv ["user_text" ],
17121809 )
17131810 timeline_items .append ((conv ["timestamp" ], "prompt" , item_html ))
17141811
0 commit comments