v0.0.42 ac6d556

session_dialog.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close