@@ -869,6 +869,124 @@ class CodemanApp {
869869 }
870870 }
871871
872+ // ═══════════════════════════════════════════════════════════════
873+ // Response Viewer — native-scroll panel for reading full Claude responses
874+ // ═══════════════════════════════════════════════════════════════
875+
876+ /** Render markdown to sanitized HTML, falling back to plain text if marked.js unavailable */
877+ _renderMarkdown ( text ) {
878+ if ( typeof marked !== 'undefined' && marked . parse ) {
879+ try {
880+ return marked . parse ( text , { breaks : true , gfm : true } ) ;
881+ } catch { /* fall through */ }
882+ }
883+ // Fallback: escape HTML and preserve whitespace
884+ const escaped = text . replace ( / & / g, '&' ) . replace ( / < / g, '<' ) . replace ( / > / g, '>' ) ;
885+ return `<pre style="white-space:pre-wrap;word-break:break-word">${ escaped } </pre>` ;
886+ }
887+
888+ async toggleResponseViewer ( ) {
889+ const viewer = document . getElementById ( 'responseViewer' ) ;
890+ const backdrop = document . getElementById ( 'responseViewerBackdrop' ) ;
891+ if ( ! viewer ) return ;
892+
893+ const isOpen = viewer . classList . contains ( 'visible' ) ;
894+ if ( isOpen ) {
895+ viewer . classList . remove ( 'visible' ) ;
896+ backdrop . classList . remove ( 'visible' ) ;
897+ return ;
898+ }
899+
900+ if ( ! this . activeSessionId ) return ;
901+ try {
902+ // Source 1: Transcript JSONL (best quality — clean structured text from Claude)
903+ const res = await fetch ( `/api/sessions/${ this . activeSessionId } /last-response` ) ;
904+ const data = await res . json ( ) ;
905+ let lastResponse = data . text || '' ;
906+
907+ // Source 2: Terminal buffer fallback (strip ANSI codes)
908+ if ( ! lastResponse ) {
909+ const termRes = await fetch ( `/api/sessions/${ this . activeSessionId } /terminal` ) ;
910+ const termData = await termRes . json ( ) ;
911+ if ( termData . terminalBuffer ) {
912+ lastResponse = termData . terminalBuffer
913+ . replace ( / \x1b \[ \? [ 0 - 9 ; ] * [ a - z A - Z ] / g, '' )
914+ . replace ( / \x1b \[ [ 0 - 9 ; ] * [ a - z A - Z ] / g, '' )
915+ . replace ( / \x1b \] [ ^ \x07 \x1b ] * (?: \x07 | \x1b \\ ) / g, '' )
916+ . replace ( / \x1b [ ( ) ] [ A - Z 0 - 9 ] / g, '' )
917+ . replace ( / \x1b [ > = < ] / g, '' )
918+ . replace ( / [ \x00 - \x08 \x0b \x0c \x0e - \x1f \x7f ] / g, '' )
919+ . replace ( / \r \n / g, '\n' ) . replace ( / \r / g, '\n' )
920+ . replace ( / [ \t ] + $ / gm, '' )
921+ . replace ( / \n { 4 , } / g, '\n\n\n' )
922+ . trim ( ) ;
923+ }
924+ }
925+
926+ const body = document . getElementById ( 'responseViewerBody' ) ;
927+ body . innerHTML = this . _renderMarkdown ( lastResponse ) ;
928+
929+ // Reset state for fresh open
930+ const title = document . getElementById ( 'responseViewerTitle' ) ;
931+ const moreBtn = document . getElementById ( 'responseViewerMore' ) ;
932+ if ( title ) title . textContent = 'Last Response' ;
933+ if ( moreBtn ) { moreBtn . style . display = '' ; moreBtn . textContent = 'More' ; }
934+
935+ viewer . classList . add ( 'visible' ) ;
936+ backdrop . classList . add ( 'visible' ) ;
937+ body . scrollTop = 0 ;
938+ } catch ( err ) {
939+ console . error ( 'Failed to load response:' , err ) ;
940+ }
941+ }
942+
943+ async loadFullContext ( ) {
944+ if ( ! this . activeSessionId ) return ;
945+ const moreBtn = document . getElementById ( 'responseViewerMore' ) ;
946+ if ( moreBtn ) moreBtn . textContent = '...' ;
947+ try {
948+ const res = await fetch ( `/api/sessions/${ this . activeSessionId } /last-response?context=full` ) ;
949+ const data = await res . json ( ) ;
950+ const messages = data . messages || [ ] ;
951+ const body = document . getElementById ( 'responseViewerBody' ) ;
952+ const title = document . getElementById ( 'responseViewerTitle' ) ;
953+ if ( ! body ) return ;
954+
955+ if ( messages . length === 0 ) {
956+ body . textContent = 'No conversation history available' ;
957+ return ;
958+ }
959+
960+ // Render conversation thread
961+ body . innerHTML = '' ;
962+ for ( const msg of messages ) {
963+ const div = document . createElement ( 'div' ) ;
964+ div . className = 'rv-message' ;
965+
966+ const role = document . createElement ( 'div' ) ;
967+ role . className = 'rv-role ' + ( msg . role === 'user' ? 'rv-role-user' : 'rv-role-assistant' ) ;
968+ role . textContent = msg . role === 'user' ? 'You' : 'Claude' ;
969+ div . appendChild ( role ) ;
970+
971+ const text = document . createElement ( 'div' ) ;
972+ text . className = 'rv-text' ;
973+ text . innerHTML = this . _renderMarkdown ( msg . text ) ;
974+ div . appendChild ( text ) ;
975+
976+ body . appendChild ( div ) ;
977+ }
978+
979+ if ( title ) title . textContent = `Conversation (${ messages . length } messages)` ;
980+ if ( moreBtn ) moreBtn . style . display = 'none' ;
981+ // Scroll to bottom (latest message)
982+ body . scrollTop = body . scrollHeight ;
983+ } catch ( err ) {
984+ console . error ( 'Failed to load context:' , err ) ;
985+ } finally {
986+ if ( moreBtn ) moreBtn . textContent = 'More' ;
987+ }
988+ }
989+
872990 async _onSessionNeedsRefresh ( ) {
873991 // Server sends this after SSE backpressure clears — terminal data was dropped,
874992 // so reload the buffer to recover from any display corruption.
0 commit comments