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