mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 08:29:42 +02:00
feat: mobile response viewer — read full Claude responses via native scroll
Claude Code's Ink framework uses alternate screen buffer + VPA cursor positioning, resulting in near-zero xterm.js scrollback on mobile. Instead of fighting terminal scrollback, this adds a native scrollable overlay that reads structured responses from Claude's JSONL transcripts. - New API: GET /api/sessions/:id/last-response reads transcript JSONL - ?context=full returns full conversation thread (user + assistant) - Fallback to terminal buffer with ANSI stripping if no transcript - Response viewer panel: bottom sheet with native iOS/Android scroll - "More" button loads full conversation context as threaded view - Eye icon in header bar (mobile only), no toolbar space impact
This commit is contained in:
@@ -869,6 +869,112 @@ class CodemanApp {
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Response Viewer — native-scroll panel for reading full Claude responses
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
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.textContent = 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.textContent = 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.
|
||||
|
||||
Reference in New Issue
Block a user