Merge #810: a Copy Markdown button in the response viewer

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-12 02:20:20 +01:00
commit b4e44c4f6a
9 files changed
+405 -11

No files matched your search

+51 -6
View File
@@ -774,6 +774,7 @@ class CodemanApp {
this.terminal = null;
this.fitAddon = null;
this.activeSessionId = null;
this._responseViewerMarkdown = '';
// ── Session detach / undock (beta) ───────────────────────────────────
// A "solo window" is a popped-out browser window showing exactly one
@@ -2948,12 +2949,7 @@ class CodemanApp {
ev.stopPropagation();
const code = copyBtn.closest('.rv-code-wrap')?.querySelector('pre code');
const ok = code ? await this._copyText(code.textContent || '') : false;
copyBtn.classList.remove('rv-copied', 'rv-copy-failed');
copyBtn.classList.add(ok ? 'rv-copied' : 'rv-copy-failed');
clearTimeout(copyBtn._resetTimer);
copyBtn._resetTimer = setTimeout(() => {
copyBtn.classList.remove('rv-copied', 'rv-copy-failed');
}, 1500);
this._setCopyControlState(copyBtn, ok);
return;
}
@@ -2997,6 +2993,50 @@ class CodemanApp {
}
}
_setCopyControlState(btn, success) {
if (!btn) return;
btn.classList.remove('rv-copied', 'rv-copy-failed');
btn.classList.add(success ? 'rv-copied' : 'rv-copy-failed');
clearTimeout(btn._resetTimer);
btn._resetTimer = setTimeout(() => {
btn.classList.remove('rv-copied', 'rv-copy-failed');
}, 1500);
}
/**
* The raw Markdown of the newest answer in a message list: every row of its
* turn, or the last row when the reader emits no numeric turns (Codex, the
* pane parser). Tool and status blocks share the assistant role but are not
* the answer.
*/
_responseViewerMarkdownFrom(messages) {
const answers = messages.filter(
(msg) => msg && msg.role === 'assistant' && (!msg.kind || msg.kind === 'response') && msg.text?.trim()
);
const last = answers[answers.length - 1];
if (!last) return '';
if (typeof last.turn !== 'number') return last.text;
return answers
.filter((msg) => msg.turn === last.turn)
.map((msg) => msg.text)
.join('\n\n');
}
/** Copy the raw Markdown source of the answer the response viewer is showing. */
async copyResponseViewerMarkdown() {
const button = document.getElementById('responseViewerCopyMarkdown');
const markdown = this._responseViewerMarkdown || '';
if (!markdown) {
this._setCopyControlState(button, false);
this.showToast('No response content to copy', 'warning');
return;
}
const copied = await this._copyText(markdown);
this._setCopyControlState(button, copied);
if (copied) this.showToast('Copied response as Markdown', 'success');
else this.showToast('Failed to copy response as Markdown', 'error');
}
/** Build one response-viewer message so the brief and full views share markup and CSS. */
_buildResponseViewerMessage(text, role, agentLabel, meta) {
const div = document.createElement('div');
@@ -3241,6 +3281,8 @@ class CodemanApp {
}
}
this._responseViewerMarkdown = this._responseViewerMarkdownFrom(turnMessages) || lastResponse;
const body = document.getElementById('responseViewerBody');
if (turnMessages.length > 0) {
// The whole last turn, rendered exactly as the full view renders that
@@ -3320,6 +3362,9 @@ class CodemanApp {
return;
}
// Keep the brief view's answer when this history holds none of its own.
this._responseViewerMarkdown = this._responseViewerMarkdownFrom(messages) || this._responseViewerMarkdown;
// Render conversation thread
const agentLabel = this._getResponseViewerAgentLabel();
body.innerHTML = '';
+5
View File
@@ -245,6 +245,11 @@
'No full conversation history available for this session': '此会话没有可显示的完整对话历史',
'Last Response': '最近一次回复',
More: '更多',
'Copy Markdown': '复制 Markdown',
'Copy response as Markdown': '将回复复制为 Markdown',
'Copied response as Markdown': '已将回复复制为 Markdown',
'Failed to copy response as Markdown': '无法将回复复制为 Markdown',
'No response content to copy': '没有可复制的回复内容',
'Codeman version': '{name}版本',
Stop: '停止',
Watching: '监视中',
+1
View File
@@ -733,6 +733,7 @@
<span id="responseViewerTitle">Last Response</span>
<div class="response-viewer-actions">
<button class="response-viewer-more" id="responseViewerMore" onclick="app.loadFullContext()">More</button>
<button class="response-viewer-more response-viewer-copy-markdown" id="responseViewerCopyMarkdown" type="button" onclick="app.copyResponseViewerMarkdown()" title="Copy response as Markdown" aria-label="Copy response as Markdown">Copy Markdown</button>
<button class="response-viewer-close" onclick="app.toggleResponseViewer()">&times;</button>
</div>
</div>
+21
View File
@@ -1804,6 +1804,27 @@ html.mobile-init .file-browser-panel {
max-height: 92dvh;
}
.response-viewer-header {
gap: 8px;
padding: 12px;
}
.response-viewer-header > span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.response-viewer-actions {
flex-shrink: 0;
gap: 4px;
}
.response-viewer-more {
padding: 3px 7px;
}
.response-viewer-body {
font-size: 14.5px;
line-height: 1.65;
+14
View File
@@ -14946,6 +14946,20 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
background: var(--control-bg-hover);
}
.response-viewer-copy-markdown {
white-space: nowrap;
}
.response-viewer-copy-markdown.rv-copied {
color: var(--green);
border-color: var(--green);
}
.response-viewer-copy-markdown.rv-copy-failed {
color: var(--red);
border-color: var(--red);
}
.response-viewer-close {
background: none;
border: none;