diff --git a/src/web/public/app.js b/src/web/public/app.js index dac3df42..a2de1649 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -869,6 +869,124 @@ class CodemanApp { } } + // ═══════════════════════════════════════════════════════════════ + // Response Viewer — native-scroll panel for reading full Claude responses + // ═══════════════════════════════════════════════════════════════ + + /** Render markdown to sanitized HTML, falling back to plain text if marked.js unavailable */ + _renderMarkdown(text) { + if (typeof marked !== 'undefined' && marked.parse) { + try { + return marked.parse(text, { breaks: true, gfm: true }); + } catch { /* fall through */ } + } + // Fallback: escape HTML and preserve whitespace + const escaped = text.replace(/&/g, '&').replace(//g, '>'); + return `
${escaped}`;
+ }
+
+ async toggleResponseViewer() {
+ const viewer = document.getElementById('responseViewer');
+ const backdrop = document.getElementById('responseViewerBackdrop');
+ if (!viewer) return;
+
+ const isOpen = viewer.classList.contains('visible');
+ if (isOpen) {
+ viewer.classList.remove('visible');
+ backdrop.classList.remove('visible');
+ return;
+ }
+
+ if (!this.activeSessionId) return;
+ try {
+ // Source 1: Transcript JSONL (best quality — clean structured text from Claude)
+ const res = await fetch(`/api/sessions/${this.activeSessionId}/last-response`);
+ const data = await res.json();
+ let lastResponse = data.text || '';
+
+ // Source 2: Terminal buffer fallback (strip ANSI codes)
+ if (!lastResponse) {
+ const termRes = await fetch(`/api/sessions/${this.activeSessionId}/terminal`);
+ const termData = await termRes.json();
+ if (termData.terminalBuffer) {
+ lastResponse = termData.terminalBuffer
+ .replace(/\x1b\[\?[0-9;]*[a-zA-Z]/g, '')
+ .replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
+ .replace(/\x1b[()][A-Z0-9]/g, '')
+ .replace(/\x1b[>=<]/g, '')
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
+ .replace(/\r\n/g, '\n').replace(/\r/g, '\n')
+ .replace(/[ \t]+$/gm, '')
+ .replace(/\n{4,}/g, '\n\n\n')
+ .trim();
+ }
+ }
+
+ const body = document.getElementById('responseViewerBody');
+ body.innerHTML = this._renderMarkdown(lastResponse);
+
+ // Reset state for fresh open
+ const title = document.getElementById('responseViewerTitle');
+ const moreBtn = document.getElementById('responseViewerMore');
+ if (title) title.textContent = 'Last Response';
+ if (moreBtn) { moreBtn.style.display = ''; moreBtn.textContent = 'More'; }
+
+ viewer.classList.add('visible');
+ backdrop.classList.add('visible');
+ body.scrollTop = 0;
+ } catch (err) {
+ console.error('Failed to load response:', err);
+ }
+ }
+
+ async loadFullContext() {
+ if (!this.activeSessionId) return;
+ const moreBtn = document.getElementById('responseViewerMore');
+ if (moreBtn) moreBtn.textContent = '...';
+ try {
+ const res = await fetch(`/api/sessions/${this.activeSessionId}/last-response?context=full`);
+ const data = await res.json();
+ const messages = data.messages || [];
+ const body = document.getElementById('responseViewerBody');
+ const title = document.getElementById('responseViewerTitle');
+ if (!body) return;
+
+ if (messages.length === 0) {
+ body.textContent = 'No conversation history available';
+ return;
+ }
+
+ // Render conversation thread
+ body.innerHTML = '';
+ for (const msg of messages) {
+ const div = document.createElement('div');
+ div.className = 'rv-message';
+
+ const role = document.createElement('div');
+ role.className = 'rv-role ' + (msg.role === 'user' ? 'rv-role-user' : 'rv-role-assistant');
+ role.textContent = msg.role === 'user' ? 'You' : 'Claude';
+ div.appendChild(role);
+
+ const text = document.createElement('div');
+ text.className = 'rv-text';
+ text.innerHTML = this._renderMarkdown(msg.text);
+ div.appendChild(text);
+
+ body.appendChild(div);
+ }
+
+ if (title) title.textContent = `Conversation (${messages.length} messages)`;
+ if (moreBtn) moreBtn.style.display = 'none';
+ // Scroll to bottom (latest message)
+ body.scrollTop = body.scrollHeight;
+ } catch (err) {
+ console.error('Failed to load context:', err);
+ } finally {
+ if (moreBtn) moreBtn.textContent = 'More';
+ }
+ }
+
async _onSessionNeedsRefresh() {
// Server sends this after SSE backpressure clears — terminal data was dropped,
// so reload the buffer to recover from any display corruption.
diff --git a/src/web/public/index.html b/src/web/public/index.html
index 42f95d3f..19bd4b11 100644
--- a/src/web/public/index.html
+++ b/src/web/public/index.html
@@ -26,6 +26,7 @@
+
@@ -92,6 +93,7 @@
--
+