@@ -388,6 +388,19 @@ export function chatHtml(): string {
388388 .preCopy:hover { opacity: 1; color: var(--an-green); border-color: var(--an-green-line); }
389389 .preCopy svg { width: 13px; height: 13px; }
390390 .preCopy.done { color: var(--an-green); opacity: 1; }
391+ /* whole-message copy: pinned top-right of an assistant reply, shown on that row's
392+ hover. Copies the raw markdown source, so pasting elsewhere keeps the formatting.
393+ pointer-events gates the hidden state so the invisible button never eats clicks
394+ or text selection at the bubble's corner. */
395+ .msgCopy { position: absolute; top: 4px; right: 4px; width: 24px; height: 24px; padding: 0;
396+ display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
397+ background: var(--an-bg-2); border: 1px solid var(--an-line-soft); border-radius: 6px;
398+ color: var(--vscode-foreground); opacity: 0; pointer-events: none;
399+ transition: opacity 0.12s, color 0.12s; z-index: 2; }
400+ .node.assistant:hover > .msgCopy { opacity: 0.75; pointer-events: auto; }
401+ .node.assistant > .msgCopy:hover { opacity: 1; color: var(--an-green); border-color: var(--an-green-line); }
402+ .node.assistant > .msgCopy.done { opacity: 1; pointer-events: auto; color: var(--an-green); }
403+ .msgCopy svg { width: 13px; height: 13px; }
391404 /* inline code IS its own copy button: a floating control would displace or cover the
392405 words around it, so the span tints on hover and flashes green once the copy lands. */
393406 .msg code.inlineCopy { cursor: pointer; transition: color 0.12s, border-color 0.12s, background 0.12s; }
@@ -2686,6 +2699,22 @@ export function chatHtml(): string {
26862699 }
26872700
26882701 // The footer under an assistant reply: elapsed time + model name (when known).
2702+ // Whole-message copy button on an assistant reply, mirroring the per-code-block
2703+ // affordance. Lives on the ROW (not inside .msg) because streaming re-renders swap
2704+ // .msg's innerHTML — a button inside it would be destroyed on every repaint.
2705+ function addMsgCopy(row, el) {
2706+ if (!row || row.querySelector(':scope > .msgCopy')) return;
2707+ const btn = document.createElement('button');
2708+ btn.className = 'msgCopy'; btn.title = 'Copy message'; btn.innerHTML = COPY_ICON;
2709+ btn.addEventListener('click', (e) => {
2710+ e.stopPropagation();
2711+ copyText(el.dataset.md || el.textContent || '', () => {
2712+ btn.classList.add('done'); btn.innerHTML = CHECK_ICON;
2713+ setTimeout(() => { btn.classList.remove('done'); btn.innerHTML = COPY_ICON; }, 1200);
2714+ });
2715+ });
2716+ row.appendChild(btn);
2717+ }
26892718 function addFooter(row, durationMs, model) {
26902719 if (durationMs == null && !model) return;
26912720 const f = document.createElement('div'); f.className = 'footer';
@@ -3159,11 +3188,12 @@ export function chatHtml(): string {
31593188 streaming.classList.remove('cursor');
31603189 cancelStreamRender(); // drop any pending live render (rAF or trailing timer)
31613190 asMd(streaming, raw);
3191+ if (msg.role === 'assistant') addMsgCopy(streaming._row, streaming);
31623192 streaming = null;
31633193 } else {
31643194 const el = bubble(msg.role, false, badge);
31653195 asMd(el, msg.text);
3166- if (msg.role === 'assistant') addFooter(el._row, msg.durationMs, msg.model); // time + model
3196+ if (msg.role === 'assistant') { addFooter(el._row, msg.durationMs, msg.model); addMsgCopy(el._row, el); } // time + model + copy
31673197 }
31683198 }
31693199 if (typingEl) tailBody().appendChild(typingEl); // keep the indicator at the thread's tail
@@ -5273,7 +5303,7 @@ export function chatHtml(): string {
52735303 const el = document . createElement ( 'div' ) ; el . className = 'msg ' + m . role ;
52745304 if ( m . role === 'assistant' ) renderMd ( el , m . text ) ; else { el . textContent = m . text ; el . dataset . md = m . text ; }
52755305 n . appendChild ( el ) ;
5276- if ( m . role === 'assistant' ) addFooter ( n , m . durationMs , m . model ) ;
5306+ if ( m . role === 'assistant' ) { addFooter ( n , m . durationMs , m . model ) ; addMsgCopy ( n , el ) ; }
52775307 }
52785308 // Insert above the current content, then restore the viewport SYNCHRONOUSLY (before the
52795309 // browser paints) so the user's position never visibly jumps. Disable smooth-scroll for
0 commit comments