44 "fmt"
55 stdlog "log"
66 "os"
7+ "sort"
78 "strconv"
89 "strings"
910 "time"
@@ -115,7 +116,11 @@ Detail View:
115116 tab / → / l: Next Tab
116117 shift+tab / ← / h: Previous Tab
117118 c : Copy Public URL (Info Tab)
118- esc / q : Back to List View
119+ enter : View Request Details (Request Log Tab)
120+ esc : Back to List View
121+
122+ Request Detail View:
123+ esc : Back to Request Log
119124`
120125
121126// viewState indicates which view is currently active
@@ -127,6 +132,7 @@ const (
127132 viewConfirmDelete // View for confirming deletion
128133 viewDetail // View showing details for a selected funnel
129134 viewHelp // View displaying keybindings/help
135+ viewRequestDetail // View showing details of a specific proxied request
130136)
131137
132138// --- Model ---
@@ -166,7 +172,8 @@ type model struct {
166172 table table.Model
167173 funnelOrder []string // Slice of funnel IDs to maintain order matching table rows
168174
169- requestTable table.Model
175+ requestTable table.Model
176+ selectedRequest * funnel.CaptureRequestResponse // The request being inspected in viewRequestDetail
170177
171178 logger * stdlog.Logger
172179}
@@ -236,6 +243,7 @@ func createRequestTable() table.Model {
236243 {Title : "Method" },
237244 {Title : "Status" },
238245 {Title : "URL" },
246+ {Title : "ID" }, // Hidden column for request ID
239247 }
240248
241249 t := table .New (
@@ -250,7 +258,6 @@ func createRequestTable() table.Model {
250258 t .SetStyles (s )
251259
252260 return t
253-
254261}
255262
256263func (m model ) Init () tea.Cmd {
@@ -273,6 +280,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
273280 // Let the input handler process 'q'
274281 break // Fall through to view-specific handlers
275282 }
283+ // Prevent quitting globally if in request detail view (let view handler decide)
284+ if m .state == viewRequestDetail && msg .String () == "q" {
285+ break // Fall through to view-specific handlers (which will ignore 'q')
286+ }
276287 return m , tea .Quit
277288 case "?" :
278289 // Don't open help from help view or create view
@@ -429,6 +440,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
429440 {Title : "Method" , Width : 8 },
430441 {Title : "Status" , Width : 8 },
431442 {Title : "URL" , Width : urlWidth },
443+ {}, // Hidden column, no title or width needed
432444 }
433445 m .requestTable .SetColumns (requestColumns )
434446
@@ -447,6 +459,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
447459 return m .updateDetailView (msg )
448460 case viewHelp :
449461 return m .updateHelpView (msg ) // Add call to new update function
462+ case viewRequestDetail :
463+ return m .updateRequestDetailView (msg )
450464 }
451465
452466 return m , nil
@@ -704,8 +718,36 @@ func (m model) updateDetailView(msg tea.Msg) (tea.Model, tea.Cmd) {
704718 }
705719 return m , nil // Do nothing if not on info tab or error
706720
707- // Note: We don't have a default case here, so keys not handled above
708- // will fall through to the logic below.
721+ case "enter" :
722+ if m .detailTabIndex == 1 {
723+ selectedRow := m .requestTable .SelectedRow ()
724+ if len (selectedRow ) < 5 { // Ensure row and ID exist (index 4)
725+ return m , nil // Or handle error
726+ }
727+ selectedRequestID := selectedRow [4 ] // Get ID from the hidden column
728+
729+ funnel , err := m .funnelRegistry .GetFunnel (m .detailedFunnelID )
730+ if err == nil {
731+ // Find the request by ID in the linked list
732+ node := funnel .Requests .Head
733+ found := false
734+ for node != nil {
735+ if node .Request .ID == selectedRequestID {
736+ m .selectedRequest = & node .Request
737+ m .state = viewRequestDetail
738+ m .requestTable .Blur () // Unfocus table when leaving
739+ found = true
740+ break
741+ }
742+ node = node .Next
743+ }
744+
745+ if found {
746+ return m , nil
747+ }
748+ }
749+ }
750+ return m , nil
709751 }
710752 }
711753
@@ -740,6 +782,24 @@ func (m model) updateHelpView(msg tea.Msg) (tea.Model, tea.Cmd) {
740782 return m , cmd
741783}
742784
785+ // updateRequestDetailView handles updates when the request detail view is active.
786+ func (m model ) updateRequestDetailView (msg tea.Msg ) (tea.Model , tea.Cmd ) {
787+ switch msg := msg .(type ) {
788+ case tea.KeyMsg :
789+ switch msg .String () {
790+ case "esc" : // Only Esc goes back to detail view (request log tab)
791+ m .state = viewDetail
792+ m .detailTabIndex = 1 // Ensure we return to the request log tab
793+ m .selectedRequest = nil // Clear the selected request
794+ m .requestTable .Focus () // Refocus the request table
795+ return m , nil
796+ }
797+ }
798+ // Handle other message types (like window resize) if needed.
799+ // Currently, no other actions are handled in this basic version.
800+ return m , nil
801+ }
802+
743803// View renders the TUI's UI. It's called after every Update.
744804func (m model ) View () string {
745805 // Footer (Render first to get its height)
@@ -766,6 +826,8 @@ func (m model) View() string {
766826 mainContent = m .viewDetailView (contentHeight )
767827 case viewHelp :
768828 mainContent = m .viewHelpView (contentHeight )
829+ case viewRequestDetail :
830+ mainContent = m .viewRequestDetailView (contentHeight )
769831 }
770832
771833 finalView := lipgloss .JoinVertical (lipgloss .Left ,
@@ -922,6 +984,7 @@ func (m *model) populateRequestTable() {
922984 node .Request .Method (),
923985 strconv .Itoa (node .Request .StatusCode ()),
924986 node .Request .Path (),
987+ node .Request .ID ,
925988 })
926989 node = node .Next
927990 }
@@ -999,6 +1062,8 @@ func (m model) footerView() string {
9991062 coreHelp = "tab/←/→: tabs, c: copy, esc/q: back, ?: help"
10001063 case viewHelp : // No specific help needed when already viewing help
10011064 coreHelp = "esc/q: back, ←/→: scroll"
1065+ case viewRequestDetail :
1066+ coreHelp = "esc/q: back"
10021067 }
10031068
10041069 // Combine status message and help text
@@ -1059,3 +1124,62 @@ func (m model) viewHelpView(contentHeight int) string {
10591124
10601125 return m .renderContent ("Help" , m .viewport .View (), contentHeight , 1 )
10611126}
1127+
1128+ // viewRequestDetailView renders the details of a selected HTTP request.
1129+ func (m model ) viewRequestDetailView (contentHeight int ) string {
1130+ title := "Request Details"
1131+ if m .selectedRequest == nil {
1132+ return m .renderContent (title , "Error: No request selected." , contentHeight , 1 )
1133+ }
1134+
1135+ requestInfo := fmt .Sprintf (
1136+ "URL: %s\n Method: %s\n Status: %d" ,
1137+ m .selectedRequest .Path (),
1138+ m .selectedRequest .Method (),
1139+ m .selectedRequest .StatusCode (),
1140+ )
1141+
1142+ formatHeaders := func (headers map [string ]string ) string {
1143+ var builder strings.Builder
1144+ if len (headers ) == 0 {
1145+ builder .WriteString (" (No headers)" )
1146+ } else {
1147+ // Get keys and sort them
1148+ keys := make ([]string , 0 , len (headers ))
1149+ for k := range headers {
1150+ keys = append (keys , k )
1151+ }
1152+ sort .Strings (keys )
1153+
1154+ // Iterate over sorted keys
1155+ for _ , k := range keys {
1156+ v := headers [k ]
1157+ builder .WriteString (fmt .Sprintf (" %s: %s\n " , k , v ))
1158+ }
1159+ // Remove trailing newline
1160+ result := builder .String ()
1161+ return strings .TrimSuffix (result , "\n " )
1162+ }
1163+ return builder .String ()
1164+ }
1165+
1166+ responseHeadersTitle := lipgloss .NewStyle ().Bold (true ).Render ("Response Headers" )
1167+ responseHeadersContent := formatHeaders (m .selectedRequest .Response .Headers )
1168+
1169+ requestHeadersTitle := lipgloss .NewStyle ().Bold (true ).Render ("Request Headers" )
1170+ requestHeadersContent := formatHeaders (m .selectedRequest .Request .Headers )
1171+
1172+ // Simple vertical layout for now
1173+ content := lipgloss .JoinVertical (lipgloss .Left ,
1174+ requestInfo ,
1175+ "\n " , // Spacer
1176+ responseHeadersTitle ,
1177+ responseHeadersContent ,
1178+ "\n " , // Spacer
1179+ requestHeadersTitle ,
1180+ requestHeadersContent ,
1181+ )
1182+
1183+ // Use the standard renderContent helper
1184+ return m .renderContent (title , content , contentHeight , 1 )
1185+ }
0 commit comments