mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
feat(response-viewer): add a Copy Markdown button
The viewer renders the agent's answer, so selecting it loses the source: headings, list markers and code fences do not survive. The only source copy was the per-code-block button. Copy Markdown copies the raw source of the newest answer the viewer holds: every row of its turn, or the last response block when the reader emits no turns. Tool and status blocks share the assistant role and are left out. The copied text is recorded where the views render, so a reopened viewer never copies an earlier answer, and an empty viewer warns instead of copying. The feedback helper replaces the inline copy of the same logic in the code-block button. On phones the header title now truncates so the longer action row stays on screen.
This commit is contained in:
1 parent
8bedbbe793
commit
992e43bc5c
9 files changed
+405
-11
No files matched your search
+51
-6
@@ -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');
|
||||
@@ -3239,6 +3279,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
|
||||
@@ -3318,6 +3360,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 = '';
|
||||
|
||||
@@ -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: '监视中',
|
||||
|
||||
@@ -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()">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1776,6 +1776,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;
|
||||
|
||||
@@ -14924,6 +14924,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;
|
||||
|
||||
Reference in new issue
Block a user