Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/response-copy-markdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"aicodeman": patch
---

Response viewer: add a Copy Markdown button that copies the raw source of the newest answer (every part of its turn), so headings, lists and code fences paste intact. An empty viewer and a refused clipboard both say so.
5 changes: 3 additions & 2 deletions docs/wiki/Mobile-Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,9 @@ Setup in [Notifications And Approvals](Notifications-And-Approvals).
## Reading long answers

The terminal viewport is small. **Last Response** (opt-in header button) renders the agent's
last answer as scrollable text instead, with a **More** button for additional context.
Persian, Arabic and Hebrew answers read right to left there.
last answer as scrollable text instead, with a **More** button for additional context and a
**Copy Markdown** button for its raw source. Persian, Arabic and Hebrew answers read right to
left there.

The [File Viewer](Working-With-Files) works on phones too, including edit mode, which is
enough to fix a typo an agent introduced while you are away from your desk.
Expand Down
8 changes: 5 additions & 3 deletions docs/wiki/Watching-Agents-Work.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ Turning on either starts the watcher.

**Last Response** (header button, opt-in) renders the agent's last answer as scrollable text
rather than terminal output. It exists mostly for phones, where reading a long answer in a
terminal viewport is painful. **More** loads additional context. Persian, Arabic and Hebrew
answers read right to left here, which the terminal cannot do, while code and the file paths
it links stay left to right.
terminal viewport is painful. **More** loads additional context. **Copy Markdown** copies the
newest answer's raw Markdown, every part of its turn, so headings, lists and code fences paste
intact (the same answer in both views; tool and status blocks are left out). Persian, Arabic
and Hebrew answers read right to left here, which the terminal cannot do, while code and the
file paths it links stay left to right.

## After the fact

Expand Down
57 changes: 51 additions & 6 deletions src/web/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = '';
Expand Down
5 changes: 5 additions & 0 deletions src/web/public/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '监视中',
Expand Down
1 change: 1 addition & 0 deletions src/web/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,7 @@ <h3 class="history-title" id="historyTitle">Resume Conversation</h3>
<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>
Expand Down
21 changes: 21 additions & 0 deletions src/web/public/mobile.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 14 additions & 0 deletions src/web/public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading