@@ -18,8 +18,8 @@ import (
18
18
"github.com/sst/opencode/internal/util"
19
19
)
20
20
21
- // NavigationDialog interface for the session navigation dialog
22
- type NavigationDialog interface {
21
+ // TimelineDialog interface for the session timeline dialog
22
+ type TimelineDialog interface {
23
23
layout.Modal
24
24
}
25
25
@@ -34,34 +34,47 @@ type RestoreToMessageMsg struct {
34
34
Index int
35
35
}
36
36
37
- // navigationItem represents a user message in the navigation list
38
- type navigationItem struct {
37
+ // timelineItem represents a user message in the timeline list
38
+ type timelineItem struct {
39
39
messageID string
40
40
content string
41
41
timestamp time.Time
42
42
index int // Index in the full message list
43
43
toolCount int // Number of tools used in this message
44
44
}
45
45
46
- func (n navigationItem ) Render (
46
+ func (n timelineItem ) Render (
47
47
selected bool ,
48
48
width int ,
49
49
isFirstInViewport bool ,
50
50
baseStyle styles.Style ,
51
+ isCurrent bool ,
51
52
) string {
52
53
t := theme .CurrentTheme ()
53
54
infoStyle := baseStyle .Background (t .BackgroundPanel ()).Foreground (t .Info ()).Render
54
55
textStyle := baseStyle .Background (t .BackgroundPanel ()).Foreground (t .Text ()).Render
55
56
57
+ // Add dot after timestamp if this is the current message - only apply color when not selected
58
+ var dot string
59
+ var dotVisualLen int
60
+ if isCurrent {
61
+ if selected {
62
+ dot = "● "
63
+ } else {
64
+ dot = lipgloss .NewStyle ().Foreground (t .Success ()).Render ("● " )
65
+ }
66
+ dotVisualLen = 2 // "● " is 2 characters wide
67
+ }
68
+
56
69
// Format timestamp - only apply color when not selected
57
70
var timeStr string
58
71
var timeVisualLen int
59
72
if selected {
60
- timeStr = n .timestamp .Format ("15:04" ) + " "
61
- timeVisualLen = lipgloss .Width (timeStr )
73
+ timeStr = n .timestamp .Format ("15:04" ) + " " + dot
74
+ timeVisualLen = lipgloss .Width (n . timestamp . Format ( "15:04" ) + " " ) + dotVisualLen
62
75
} else {
63
- timeStr = infoStyle (n .timestamp .Format ("15:04" ) + " " )
64
- timeVisualLen = lipgloss .Width (timeStr )
76
+ timeStr = infoStyle (n .timestamp .Format ("15:04" )+ " " ) + dot
77
+ timeVisualLen = lipgloss .Width (n . timestamp . Format ( "15:04" ) + " " ) + dotVisualLen
65
78
}
66
79
67
80
// Tool count display (fixed width for alignment) - only apply color when not selected
@@ -78,7 +91,7 @@ func (n navigationItem) Render(
78
91
}
79
92
80
93
// Calculate available space for content
81
- // Reserve space for: timestamp + space + toolInfo + padding + some buffer
94
+ // Reserve space for: timestamp + dot + space + toolInfo + padding + some buffer
82
95
reservedSpace := timeVisualLen + 1 + toolInfoVisualLen + 4
83
96
contentWidth := max (width - reservedSpace , 8 )
84
97
@@ -135,23 +148,23 @@ func (n navigationItem) Render(
135
148
return itemStyle .Render (text )
136
149
}
137
150
138
- func (n navigationItem ) Selectable () bool {
151
+ func (n timelineItem ) Selectable () bool {
139
152
return true
140
153
}
141
154
142
- type navigationDialog struct {
155
+ type timelineDialog struct {
143
156
width int
144
157
height int
145
158
modal * modal.Modal
146
- list list.List [navigationItem ]
159
+ list list.List [timelineItem ]
147
160
app * app.App
148
161
}
149
162
150
- func (n * navigationDialog ) Init () tea.Cmd {
163
+ func (n * timelineDialog ) Init () tea.Cmd {
151
164
return nil
152
165
}
153
166
154
- func (n * navigationDialog ) Update (msg tea.Msg ) (tea.Model , tea.Cmd ) {
167
+ func (n * timelineDialog ) Update (msg tea.Msg ) (tea.Model , tea.Cmd ) {
155
168
switch msg := msg .(type ) {
156
169
case tea.WindowSizeMsg :
157
170
n .width = msg .Width
@@ -163,7 +176,7 @@ func (n *navigationDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
163
176
// Handle navigation and immediately scroll to selected message
164
177
var cmd tea.Cmd
165
178
listModel , cmd := n .list .Update (msg )
166
- n .list = listModel .(list.List [navigationItem ])
179
+ n .list = listModel .(list.List [timelineItem ])
167
180
168
181
// Get the newly selected item and scroll to it immediately
169
182
if item , idx := n .list .GetSelectedItem (); idx >= 0 {
@@ -191,11 +204,11 @@ func (n *navigationDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
191
204
192
205
var cmd tea.Cmd
193
206
listModel , cmd := n .list .Update (msg )
194
- n .list = listModel .(list.List [navigationItem ])
207
+ n .list = listModel .(list.List [timelineItem ])
195
208
return n , cmd
196
209
}
197
210
198
- func (n * navigationDialog ) Render (background string ) string {
211
+ func (n * timelineDialog ) Render (background string ) string {
199
212
listView := n .list .View ()
200
213
201
214
t := theme .CurrentTheme ()
@@ -229,7 +242,7 @@ func (n *navigationDialog) Render(background string) string {
229
242
return n .modal .Render (content , background )
230
243
}
231
244
232
- func (n * navigationDialog ) Close () tea.Cmd {
245
+ func (n * timelineDialog ) Close () tea.Cmd {
233
246
return nil
234
247
}
235
248
@@ -268,17 +281,17 @@ func countToolsInResponse(messages []app.Message, userMessageIndex int) int {
268
281
return count
269
282
}
270
283
271
- // NewNavigationDialog creates a new session navigation dialog
272
- func NewNavigationDialog (app * app.App ) NavigationDialog {
273
- var items []navigationItem
284
+ // NewTimelineDialog creates a new session timeline dialog
285
+ func NewTimelineDialog (app * app.App ) TimelineDialog { // renamed from NewNavigationDialog
286
+ var items []timelineItem
274
287
275
288
// Filter to only user messages and extract relevant info
276
289
for i , message := range app .Messages {
277
290
if userMsg , ok := message .Info .(opencode.UserMessage ); ok {
278
291
preview := extractMessagePreview (message .Parts )
279
292
toolCount := countToolsInResponse (app .Messages , i )
280
293
281
- items = append (items , navigationItem {
294
+ items = append (items , timelineItem {
282
295
messageID : userMsg .ID ,
283
296
content : preview ,
284
297
timestamp : time .UnixMilli (int64 (userMsg .Time .Created )),
@@ -290,25 +303,50 @@ func NewNavigationDialog(app *app.App) NavigationDialog {
290
303
291
304
listComponent := list .NewListComponent (
292
305
list .WithItems (items ),
293
- list.WithMaxVisibleHeight [navigationItem ](12 ),
294
- list.WithFallbackMessage [navigationItem ]("No user messages in this session" ),
295
- list.WithAlphaNumericKeys [navigationItem ](true ),
306
+ list.WithMaxVisibleHeight [timelineItem ](12 ),
307
+ list.WithFallbackMessage [timelineItem ]("No user messages in this session" ),
308
+ list.WithAlphaNumericKeys [timelineItem ](true ),
296
309
list .WithRenderFunc (
297
- func (item navigationItem , selected bool , width int , baseStyle styles.Style ) string {
298
- return item .Render (selected , width , false , baseStyle )
310
+ func (item timelineItem , selected bool , width int , baseStyle styles.Style ) string {
311
+ // Determine if this item is the current message for the session
312
+ isCurrent := false
313
+ if app .Session .Revert .MessageID != "" {
314
+ // When reverted, Session.Revert.MessageID contains the NEXT user message ID
315
+ // So we need to find the previous user message to highlight the correct one
316
+ for i , navItem := range items {
317
+ if navItem .messageID == app .Session .Revert .MessageID && i > 0 {
318
+ // Found the next message, so the previous one is current
319
+ isCurrent = item .messageID == items [i - 1 ].messageID
320
+ break
321
+ }
322
+ }
323
+ } else if len (app .Messages ) > 0 {
324
+ // If not reverted, highlight the last user message
325
+ lastUserMsgID := ""
326
+ for i := len (app .Messages ) - 1 ; i >= 0 ; i -- {
327
+ if userMsg , ok := app .Messages [i ].Info .(opencode.UserMessage ); ok {
328
+ lastUserMsgID = userMsg .ID
329
+ break
330
+ }
331
+ }
332
+ isCurrent = item .messageID == lastUserMsgID
333
+ }
334
+ // Only show the dot if undo/redo/restore is available
335
+ showDot := app .Session .Revert .MessageID != ""
336
+ return item .Render (selected , width , false , baseStyle , isCurrent && showDot )
299
337
},
300
338
),
301
- list .WithSelectableFunc (func (item navigationItem ) bool {
339
+ list .WithSelectableFunc (func (item timelineItem ) bool {
302
340
return true
303
341
}),
304
342
)
305
343
listComponent .SetMaxWidth (layout .Current .Container .Width - 12 )
306
344
307
- return & navigationDialog {
345
+ return & timelineDialog {
308
346
list : listComponent ,
309
347
app : app ,
310
348
modal : modal .New (
311
- modal .WithTitle ("Jump to Message " ),
349
+ modal .WithTitle ("Session Timeline " ),
312
350
modal .WithMaxWidth (layout .Current .Container .Width - 8 ),
313
351
),
314
352
}
0 commit comments