1package modes
2
3import (
4 "fmt"
5 "strings"
6 "time"
7
8 "github.com/patriceckhart/zot/internal/core"
9 "github.com/patriceckhart/zot/internal/tui"
10)
11
12// sessionDialog is the inline picker shown when the user runs /sessions.
13type sessionDialog struct {
14 active bool
15 sessions []core.SessionSummary
16 cursor int
17}
18
19// sessionDialogAction is returned by HandleKey.
20type sessionDialogAction struct {
21 Select bool
22 Path string
23 Close bool
24}
25
26func newSessionDialog() *sessionDialog { return &sessionDialog{} }
27
28// Open populates the dialog from root + cwd and shows it. Empty
29// sessions (zero messages) are filtered out so the currently-running
30// session, a freshly-opened one that hasn't received a prompt yet,
31// and any stale empties that haven't been pruned yet all stay out
32// of the picker. Resuming an empty session is a no-op anyway.
33func (d *sessionDialog) Open(root, cwd string) {
34 all := core.DescribeSessions(root, cwd)
35 filtered := make([]core.SessionSummary, 0, len(all))
36 for _, s := range all {
37 if s.MessageCount == 0 {
38 continue
39 }
40 filtered = append(filtered, s)
41 }
42 d.sessions = filtered
43 d.cursor = 0
44 d.active = true
45}
46
47// Close hides the dialog.
48func (d *sessionDialog) Close() { d.active = false }
49
50// Active reports whether the dialog is visible and consumes input.
51func (d *sessionDialog) Active() bool { return d != nil && d.active }
52
53// Render returns the dialog lines.
54func (d *sessionDialog) Render(th tui.Theme, width int) []string {
55 if !d.Active() {
56 return nil
57 }
58 var lines []string
59 lines = append(lines, frameHeader(th, "sessions", width))
60 if len(d.sessions) == 0 {
61 lines = append(lines, th.FG256(th.Muted, "no previous sessions for this directory"))
62 lines = append(lines, th.FG256(th.Muted, "press esc to close"))
63 lines = append(lines, frameRule(th, width))
64 return lines
65 }
66 lines = append(lines, th.FG256(th.Muted, "pick a session to resume (↑/↓, enter, esc to cancel)"))
67 for i, s := range d.sessions {
68 plain := " " + formatSessionRowPlain(s, width-2)
69 if i == d.cursor {
70 lines = append(lines, th.PadHighlight(plain, width))
71 } else {
72 lines = append(lines, th.FG256(th.Muted, plain))
73 }
74 }
75 lines = append(lines, frameRule(th, width))
76 return lines
77}
78
79// formatSessionRowPlain returns the session row body without any ANSI
80// styling so the caller can wrap it in either a plain mute color or a
81// full-row selection highlight.
82func formatSessionRowPlain(s core.SessionSummary, maxWidth int) string {
83 when := formatRelative(s.Started)
84 summary := strings.TrimSpace(s.FirstUserText)
85 if summary == "" {
86 summary = "(empty)"
87 }
88 left := fmt.Sprintf("%-14s %s/%s %d msgs $%.4f ",
89 when, s.Provider, s.Model, s.MessageCount, s.TotalCost)
90 room := maxWidth - len(left)
91 if room < 10 {
92 room = 10
93 }
94 if len(summary) > room {
95 summary = summary[:room-1] + "…"
96 }
97 summary = strings.ReplaceAll(summary, "\n", " ")
98 return left + summary
99}
100
101func formatRelative(t time.Time) string {
102 if t.IsZero() {
103 return "unknown"
104 }
105 d := time.Since(t)
106 switch {
107 case d < time.Minute:
108 return "just now"
109 case d < time.Hour:
110 return fmt.Sprintf("%d min ago", int(d.Minutes()))
111 case d < 24*time.Hour:
112 return fmt.Sprintf("%d h ago", int(d.Hours()))
113 case d < 7*24*time.Hour:
114 return fmt.Sprintf("%d d ago", int(d.Hours()/24))
115 default:
116 return t.Local().Format("2006-01-02")
117 }
118}
119
120// HandleKey advances the dialog and returns an action to apply, if any.
121func (d *sessionDialog) HandleKey(k tui.Key) sessionDialogAction {
122 switch k.Kind {
123 case tui.KeyUp:
124 if d.cursor > 0 {
125 d.cursor--
126 }
127 case tui.KeyDown:
128 if d.cursor < len(d.sessions)-1 {
129 d.cursor++
130 }
131 case tui.KeyEsc:
132 d.Close()
133 return sessionDialogAction{Close: true}
134 case tui.KeyEnter:
135 if len(d.sessions) == 0 {
136 d.Close()
137 return sessionDialogAction{Close: true}
138 }
139 s := d.sessions[d.cursor]
140 d.Close()
141 return sessionDialogAction{Select: true, Path: s.Path}
142 }
143 return sessionDialogAction{}
144}