fix: move Copy/Insert/Replace toolbar INSIDE each code block

The previous commit put the toolbar at the top of every assistant
message. The intent was actually to put the buttons inside the
fenced code blocks (the parts wrapped in ```python etc.), not above
the whole result section.

Changes:
  - opencode_inline_prompt.ts: removed the message-level toolbar on
    .opencode-msg-assistant. Instead, after marked.parse the assistant
    markdown, for every rendered <pre> we wrap it in
    .opencode-code-block and prepend a small toolbar with the same
    three buttons. Each button acts on the code text of THAT specific
    block (snapshot of <code>.textContent taken before DOM mutation,
    so the toolbar label text never leaks into the copied/inserted/
    replaced value). Messages with no code fences get no toolbar.
  - style/base.css: replaced .opencode-msg-toolbar / .opencode-msg-btn
    with .opencode-code-block / .opencode-code-toolbar /
    .opencode-code-btn. The wrapped <pre> connects visually to the
    toolbar (no top border/radius, no top margin) so each fenced
    block reads as one unit (GitHub-style).
  - Tests: updated the marked mock to produce real <pre><code> for
    fenced code (so the per-block post-processing can find them)
    and <h1>/<p> for headings. The 4 button tests now look for the
    toolbar inside .opencode-code-block and assert the button acts
    on the code block text ('print(1)'), not the whole markdown.
    Added a test that an assistant message with no code fences has
    no toolbar.

Verification: pytest 37/37, jest 34/34, build OK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
tao.chen
2026-07-23 19:19:29 +08:00
co-authored by Claude Fable 5
parent cb3576c83a
commit b5642ec5cd
3 changed files with 178 additions and 65 deletions
+54 -32
View File
@@ -225,40 +225,62 @@ export class OpenCodeInlinePrompt extends Widget {
if (msg.role === 'user') {
wrap.textContent = msg.content;
} else {
// Assistant: small action toolbar (copy / insert at cursor /
// replace cell) + the rendered markdown content below.
const toolbar = document.createElement('div');
toolbar.className = 'opencode-msg-toolbar';
const content = msg.content;
const mkBtn = (label: string, title: string, onClick: () => void) => {
const b = document.createElement('button');
b.type = 'button';
b.className = 'opencode-msg-btn';
b.textContent = label;
b.title = title;
b.addEventListener('click', onClick);
return b;
};
toolbar.appendChild(
mkBtn('📋 复制', '复制内容到剪贴板', () => {
void this._copyToClipboard(content);
})
);
toolbar.appendChild(
mkBtn('↪ 插入', '在光标位置插入内容', () => {
this._insertAtCursor(content);
})
);
toolbar.appendChild(
mkBtn('⟳ 替换', '替换整个单元格内容', () => {
this._replaceCell(content);
})
);
wrap.appendChild(toolbar);
// Assistant: render the markdown, then for every fenced code
// block (<pre>) attach a small toolbar with Copy / Insert /
// Replace buttons that act on THAT block's code (not the whole
// message). Messages without code fences get no toolbar.
const rendered = document.createElement('div');
rendered.className = 'opencode-msg-content';
rendered.innerHTML = marked.parse(content) as string;
rendered.innerHTML = marked.parse(msg.content) as string;
const codeBlocks = rendered.querySelectorAll('pre');
codeBlocks.forEach(pre => {
const code = pre.querySelector('code');
if (!code) {
return;
}
// Snapshot the code text before we mutate the DOM so the
// buttons act on the exact code inside this block (no
// surrounding ```fences```, no toolbar label text).
const codeText = code.textContent ?? '';
const wrap = document.createElement('div');
wrap.className = 'opencode-code-block';
const toolbar = document.createElement('div');
toolbar.className = 'opencode-code-toolbar';
const mkBtn = (
label: string,
title: string,
onClick: () => void
) => {
const b = document.createElement('button');
b.type = 'button';
b.className = 'opencode-code-btn';
b.textContent = label;
b.title = title;
b.addEventListener('click', onClick);
return b;
};
toolbar.appendChild(
mkBtn('📋 复制', '复制代码', () => {
void this._copyToClipboard(codeText);
})
);
toolbar.appendChild(
mkBtn('↪ 插入', '在光标位置插入', () => {
this._insertAtCursor(codeText);
})
);
toolbar.appendChild(
mkBtn('⟳ 替换', '替换单元格内容', () => {
this._replaceCell(codeText);
})
);
wrap.appendChild(toolbar);
// Move the original <pre> into the wrapper.
pre.parentNode!.insertBefore(wrap, pre);
wrap.appendChild(pre);
});
wrap.appendChild(rendered);
}
this._historyEl.appendChild(wrap);