Skip to content

Commit dc158c9

Browse files
committed
Add inline message expansion in Texts timeline view
Press Enter on a message in the timeline to expand it and show the full body text with word wrapping. Press Enter again or Esc to collapse. The full body is fetched from message_bodies via Engine.GetMessage.
1 parent 2180ad9 commit dc158c9

5 files changed

Lines changed: 101 additions & 2 deletions

File tree

internal/tui/model.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
767767
return m.handleTextSearchResult(msg)
768768
case textStatsLoadedMsg:
769769
return m.handleTextStatsLoaded(msg)
770+
case textMessageBodyMsg:
771+
if msg.err != nil {
772+
return m, nil // silently ignore body fetch errors
773+
}
774+
if msg.idx == m.textState.expandedIdx {
775+
m.textState.expandedBody = msg.body
776+
}
777+
return m, nil
770778
}
771779
return m, nil
772780
}

internal/tui/text_commands.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ type textSearchResultMsg struct {
3434
err error
3535
}
3636

37+
// textMessageBodyMsg is sent when a message body is fetched for expansion.
38+
type textMessageBodyMsg struct {
39+
idx int // index in m.textState.messages
40+
body string // full body text
41+
err error
42+
}
43+
3744
// textStatsLoadedMsg is sent when text stats are loaded.
3845
type textStatsLoadedMsg struct {
3946
stats *query.TotalStats
@@ -151,3 +158,27 @@ func (m Model) loadTextData() tea.Cmd {
151158
return m.loadTextAggregate()
152159
}
153160
}
161+
162+
// loadTextMessageBody fetches the full body of a message for inline expansion.
163+
func (m Model) loadTextMessageBody(msgID int64, idx int) tea.Cmd {
164+
eng := m.engine
165+
return safeCmdWithPanic(
166+
func() tea.Msg {
167+
detail, err := eng.GetMessage(
168+
context.Background(), msgID,
169+
)
170+
if err != nil {
171+
return textMessageBodyMsg{idx: idx, err: err}
172+
}
173+
return textMessageBodyMsg{
174+
idx: idx, body: detail.BodyText,
175+
}
176+
},
177+
func(r any) tea.Msg {
178+
return textMessageBodyMsg{
179+
idx: idx,
180+
err: fmt.Errorf("fetch body panic: %v", r),
181+
}
182+
},
183+
)
184+
}

internal/tui/text_keys.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,31 @@ func (m Model) handleTextTimelineKeys(
112112
msg tea.KeyMsg,
113113
) (tea.Model, tea.Cmd) {
114114
switch msg.String() {
115+
case "enter":
116+
// Toggle inline expansion of the current message
117+
idx := m.textState.cursor
118+
if idx < 0 || idx >= len(m.textState.messages) {
119+
return m, nil
120+
}
121+
if m.textState.expandedIdx == idx {
122+
// Collapse
123+
m.textState.expandedIdx = -1
124+
m.textState.expandedBody = ""
125+
return m, nil
126+
}
127+
// Expand — fetch full body
128+
m.textState.expandedIdx = idx
129+
m.textState.expandedBody = "" // loading
130+
msgID := m.textState.messages[idx].ID
131+
return m, m.loadTextMessageBody(msgID, idx)
132+
115133
case "esc", "backspace":
134+
if m.textState.expandedIdx >= 0 {
135+
// Close expansion first
136+
m.textState.expandedIdx = -1
137+
m.textState.expandedBody = ""
138+
return m, nil
139+
}
116140
return m.textGoBack()
117141

118142
case "j", "down":
@@ -383,6 +407,8 @@ func (m Model) textDrillDown() (tea.Model, tea.Cmd) {
383407
m.textState.level = textLevelTimeline
384408
m.textState.cursor = 0
385409
m.textState.scrollOffset = 0
410+
m.textState.expandedIdx = -1
411+
m.textState.expandedBody = ""
386412
m.loading = true
387413
return m, m.loadTextMessages()
388414

internal/tui/text_state.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ type textState struct {
3333
filter query.TextFilter
3434
stats *query.TotalStats
3535
breadcrumbs []textNavSnapshot
36+
37+
// Inline message expansion
38+
expandedIdx int // index of expanded message (-1 = none)
39+
expandedBody string // full body text of expanded message
3640
}
3741

3842
// textNavSnapshot stores state for text mode navigation history.

internal/tui/text_view.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,36 @@ func (m Model) textTimelineView() string {
537537
style.Render(padRight(line, m.width-3)),
538538
)
539539
sb.WriteString("\n")
540+
541+
// Inline expansion: show full wrapped body below this row
542+
if i == m.textState.expandedIdx {
543+
bodyText := m.textState.expandedBody
544+
if bodyText == "" {
545+
bodyText = "(loading...)"
546+
}
547+
bodyText = textutil.SanitizeTerminal(bodyText)
548+
indent := strings.Repeat(" ", indicatorWidth)
549+
wrapWidth := m.width - indicatorWidth - 2
550+
if wrapWidth < 20 {
551+
wrapWidth = 20
552+
}
553+
for _, wline := range wrapText(bodyText, wrapWidth) {
554+
sb.WriteString(indent)
555+
sb.WriteString(
556+
style.Render(
557+
padRight(wline, m.width-indicatorWidth),
558+
),
559+
)
560+
sb.WriteString("\n")
561+
}
562+
// Blank separator after expanded body
563+
sb.WriteString(
564+
normalRowStyle.Render(
565+
strings.Repeat(" ", m.width),
566+
),
567+
)
568+
sb.WriteString("\n")
569+
}
540570
}
541571

542572
// Fill remaining space
@@ -596,8 +626,8 @@ func (m Model) textFooterView() string {
596626

597627
case textLevelTimeline:
598628
keys = []string{
599-
"\u2191/\u2193 navigate", "Esc back",
600-
"m email", "? help",
629+
"\u2191/\u2193 navigate", "Enter expand",
630+
"Esc back", "m email", "? help",
601631
}
602632
n := len(m.textState.messages)
603633
if n > 0 {

0 commit comments

Comments
 (0)