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.
29func (d *sessionDialog) Open(root, cwd string) {
30 d.sessions = core.DescribeSessions(root, cwd)
31 d.cursor = 0
32 d.active = true
33}
34
35// Close hides the dialog.
36func (d *sessionDialog) Close() { d.active = false }
37
38// Active reports whether the dialog is visible and consumes input.
39func (d *sessionDialog) Active() bool { return d != nil && d.active }
40
41// Render returns the dialog lines.
42func (d *sessionDialog) Render(th tui.Theme, width int) []string {
43 if !d.Active() {
44 return nil
45 }
46 var lines []string
47 lines = append(lines, frameHeader(th, "sessions", width))
48 if len(d.sessions) == 0 {
49 lines = append(lines, th.FG256(th.Muted, "no previous sessions for this directory"))
50 lines = append(lines, th.FG256(th.Muted, "press esc to close"))
51 lines = append(lines, frameRule(th, width))
52 return lines
53 }
54 lines = append(lines, th.FG256(th.Muted, "pick a session to resume (↑/↓, enter, esc to cancel)"))
55 for i, s := range d.sessions {
56 plain := " " + formatSessionRowPlain(s, width-2)
57 if i == d.cursor {
58 lines = append(lines, th.PadHighlight(plain, width))
59 } else {
60 lines = append(lines, th.FG256(th.Muted, plain))
61 }
62 }
63 lines = append(lines, frameRule(th, width))
64 return lines
65}
66
67// formatSessionRowPlain returns the session row body without any ANSI
68// styling so the caller can wrap it in either a plain mute color or a
69// full-row selection highlight.
70func formatSessionRowPlain(s core.SessionSummary, maxWidth int) string {
71 when := formatRelative(s.Started)
72 summary := strings.TrimSpace(s.FirstUserText)
73 if summary == "" {
74 summary = "(empty)"
75 }
76 left := fmt.Sprintf("%-14s %s/%s %d msgs $%.4f ",
77 when, s.Provider, s.Model, s.MessageCount, s.TotalCost)
78 room := maxWidth - len(left)
79 if room < 10 {
80 room = 10
81 }
82 if len(summary) > room {
83 summary = summary[:room-1] + "…"
84 }
85 summary = strings.ReplaceAll(summary, "\n", " ")
86 return left + summary
87}
88
89func formatRelative(t time.Time) string {
90 if t.IsZero() {
91 return "unknown"
92 }
93 d := time.Since(t)
94 switch {
95 case d < time.Minute:
96 return "just now"
97 case d < time.Hour:
98 return fmt.Sprintf("%d min ago", int(d.Minutes()))
99 case d < 24*time.Hour:
100 return fmt.Sprintf("%d h ago", int(d.Hours()))
101 case d < 7*24*time.Hour:
102 return fmt.Sprintf("%d d ago", int(d.Hours()/24))
103 default:
104 return t.Local().Format("2006-01-02")
105 }
106}
107
108// HandleKey advances the dialog and returns an action to apply, if any.
109func (d *sessionDialog) HandleKey(k tui.Key) sessionDialogAction {
110 switch k.Kind {
111 case tui.KeyUp:
112 if d.cursor > 0 {
113 d.cursor--
114 }
115 case tui.KeyDown:
116 if d.cursor < len(d.sessions)-1 {
117 d.cursor++
118 }
119 case tui.KeyEsc:
120 d.Close()
121 return sessionDialogAction{Close: true}
122 case tui.KeyEnter:
123 if len(d.sessions) == 0 {
124 d.Close()
125 return sessionDialogAction{Close: true}
126 }
127 s := d.sessions[d.cursor]
128 d.Close()
129 return sessionDialogAction{Select: true, Path: s.Path}
130 }
131 return sessionDialogAction{}
132}