feat: add thinking card copy button
This commit is contained in:
@@ -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
|
||||
|
||||
BIN
docs/pr-media/2449/after-thinking-copy.png
Normal file
BIN
docs/pr-media/2449/after-thinking-copy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/pr-media/2449/before-thinking-copy.png
Normal file
BIN
docs/pr-media/2449/before-thinking-copy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -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;}
|
||||
|
||||
19
static/ui.js
19
static/ui.js
@@ -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;
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user