feat: add thinking card copy button

This commit is contained in:
Michael Lam
2026-05-17 03:57:07 -07:00
parent 603183a301
commit 2785065a09
6 changed files with 38 additions and 6 deletions

View File

@@ -2,6 +2,10 @@
## [Unreleased]
### Added
- **PR #2460** by @Michaelyklam (closes #2449) — Add a copy button to Thinking card headers so users can copy the card's reasoning text without selecting the `<pre>` manually. The button stops header-toggle propagation and shows the same short checkmark feedback pattern used by existing copy actions.
## [v0.51.82] — 2026-05-17 — Release BF (stage-375 — 2-PR batch — table renderer pipe protection + Catppuccin appearance skin)
### Added

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -3057,7 +3057,10 @@ main.main.showing-logs > #mainLogs{display:flex;}
Only sub-element selectors that the consolidated block doesn't cover
(label, toggle, rotate animation) are kept here. ── */
.thinking-card-label{font-weight:600;letter-spacing:.02em;}
.thinking-card-toggle{margin-left:auto;font-size:10px;display:inline-flex;align-items:center;justify-content:center;transform-origin:center;transition:transform .18s ease;will-change:transform;}
.thinking-card-btn-row{margin-left:auto;display:inline-flex;align-items:center;gap:6px;}
.thinking-copy-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--accent-text);opacity:.72;cursor:pointer;transition:background .15s,color .15s,opacity .15s;}
.thinking-copy-btn:hover,.thinking-copy-btn:focus-visible{background:var(--accent-bg-strong);opacity:1;outline:none;}
.thinking-card-toggle{font-size:10px;display:inline-flex;align-items:center;justify-content:center;transform-origin:center;transition:transform .18s ease;will-change:transform;}
.thinking-card.open .thinking-card-toggle{transform:rotate(90deg);}
.bg-error-banner{background:rgba(229,62,62,.15);border:1px solid rgba(229,62,62,.3);color:#fca5a5;padding:8px 16px;font-size:12px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-radius:0;}

View File

@@ -3682,6 +3682,19 @@ function copyMsg(btn){
setTimeout(()=>{btn.innerHTML=orig;btn.style.color='';},1500);
}).catch(()=>showToast(t('copy_failed')));
}
function _copyThinkingText(btn){
const card=btn&&btn.closest?btn.closest('.thinking-card'):null;
if(!card)return;
const pre=card.querySelector('.thinking-card-body pre');
const text=pre?pre.textContent:'';
if(!text)return;
_copyText(text).then(()=>{
const orig=btn.innerHTML;
btn.innerHTML=li('check',12);
btn.style.color='var(--accent)';
setTimeout(()=>{btn.innerHTML=orig;btn.style.color='';},1500);
}).catch(()=>showToast(t('copy_failed')));
}
// ── TTS: Text-to-Speech via Web Speech API (#499) ──
// Strips markdown, code blocks, and MEDIA: paths for clean speech output.
@@ -4732,9 +4745,9 @@ function _assistantTurnBlocks(turn){
}
function _thinkingCardHtml(text, open){
const clean=_sanitizeThinkingDisplayText(text);
return open
? `<div class="thinking-card open"><div class="thinking-card-header" onclick="this.parentElement.classList.toggle('open')"><span class="thinking-card-icon">${li('lightbulb',14)}</span><span class="thinking-card-label">${t('thinking')}</span><span class="thinking-card-toggle">${li('chevron-right',12)}</span></div><div class="thinking-card-body"><pre>${esc(clean)}</pre></div></div>`
: `<div class="thinking-card"><div class="thinking-card-header" onclick="this.parentElement.classList.toggle('open')"><span class="thinking-card-icon">${li('lightbulb',14)}</span><span class="thinking-card-label">${t('thinking')}</span><span class="thinking-card-toggle">${li('chevron-right',12)}</span></div><div class="thinking-card-body"><pre>${esc(clean)}</pre></div></div>`;
const copyBtn=`<button class="thinking-copy-btn" onclick="event.stopPropagation();_copyThinkingText(this)" title="${t('copy')}" aria-label="${t('copy')}">${li('copy',12)}</button>`;
const classes=`thinking-card${open?' open':''}`;
return `<div class="${classes}"><div class="thinking-card-header" onclick="this.parentElement.classList.toggle('open')"><span class="thinking-card-icon">${li('lightbulb',14)}</span><span class="thinking-card-label">${t('thinking')}</span><span class="thinking-card-btn-row">${copyBtn}<span class="thinking-card-toggle">${li('chevron-right',12)}</span></span></div><div class="thinking-card-body"><pre>${esc(clean)}</pre></div></div>`;
}
function isSimplifiedToolCalling(){
return window._simplifiedToolCalling!==false;

View File

@@ -27,7 +27,8 @@ def test_tool_card_detail_uses_transitionable_collapsed_state():
def test_thinking_card_toggle_and_body_use_animation_friendly_state():
assert ".thinking-card-toggle{margin-left:auto;font-size:10px;display:inline-flex;" in COMPACT_CSS
assert ".thinking-card-btn-row{margin-left:auto;display:inline-flex;align-items:center;gap:6px;" in COMPACT_CSS
assert ".thinking-card-toggle{font-size:10px;display:inline-flex;" in COMPACT_CSS
assert ".thinking-card-header{display:flex;align-items:center;gap:8px;" in COMPACT_CSS
# Body uses div default (display:block); canonical rule lives in the
# consolidated block. Open state caps at 260px (intentional "quieter" sizing).
@@ -41,7 +42,18 @@ def test_thinking_card_toggle_and_body_use_animation_friendly_state():
def test_tool_card_toggle_uses_same_chevron_icon_markup_as_thinking_card():
assert "<span class=\"thinking-card-toggle\">${li('chevron-right',12)}</span>" in UI_JS
assert "<span class=\"tool-card-toggle\">${li('chevron-right',12)}</span>" in UI_JS
assert "<div class=\"thinking-card\"><div class=\"thinking-card-header\" onclick=\"this.parentElement.classList.toggle('open')\"><span class=\"thinking-card-icon\">" in UI_JS
assert "<div class=\"${classes}\"><div class=\"thinking-card-header\" onclick=\"this.parentElement.classList.toggle('open')\"><span class=\"thinking-card-icon\">" in UI_JS
def test_thinking_card_header_includes_copy_button_that_does_not_toggle_card():
assert "function _copyThinkingText(btn){" in UI_JS
assert "const copyBtn=`<button class=\"thinking-copy-btn\"" in UI_JS
assert "event.stopPropagation();_copyThinkingText(this)" in UI_JS
assert "card.querySelector('.thinking-card-body pre')" in UI_JS
assert "_copyText(text).then(()=>{" in UI_JS
assert "btn.innerHTML=li('check',12);" in UI_JS
assert ".thinking-copy-btn{" in COMPACT_CSS
assert ".thinking-copy-btn:hover,.thinking-copy-btn:focus-visible{" in COMPACT_CSS
def test_live_thinking_updates_existing_card_body_in_place():