v0.46.0 ea0e112

session_info.go RawHistoryFolder

1package modes
2
3import (
4	"fmt"
5	"path/filepath"
6	"strings"
7
8	"github.com/bnema/zut/packages/core"
9	"github.com/bnema/zut/packages/tui"
10	"github.com/mattn/go-runewidth"
11)
12
13type sessionInfoBlock struct {
14	afterMessage int
15	lines        []string
16}
17
18// renderSessionInfoBlock formats information owned by the live interactive
19// session. Paths remain absolute so users can copy the transcript location.
20func renderSessionInfoBlock(th tui.Theme, width int, sessionID, sessionPath, cwd, provider, model string) []string {
21	if width < 20 {
22		width = 20
23	}
24
25	rows := [][2]string{
26		{"session id", valueOrUnavailable(sessionID)},
27		{"session file", valueOrUnavailable(sessionPath)},
28		{"working directory", valueOrUnavailable(cwd)},
29		{"provider", valueOrUnavailable(provider)},
30		{"model", valueOrUnavailable(model)},
31	}
32	labelWidth := 14
33	for _, row := range rows {
34		if n := runewidth.StringWidth(row[0]); n > labelWidth {
35			labelWidth = n
36		}
37	}
38
39	pad := func(label string) string {
40		return label + strings.Repeat(" ", labelWidth-runewidth.StringWidth(label))
41	}
42
43	out := []string{frameHeader(th, "session info", width), ""}
44	for _, row := range rows {
45		out = append(out, fmt.Sprintf("  %s  %s",
46			th.FGColor(th.Accent, pad(row[0])),
47			th.FGColor(th.Muted, row[1])))
48	}
49	return append(out, "", frameRule(th, width), "")
50}
51
52func insertSessionInfoBlocks(rows []string, anchors []tui.MessageAnchor, blocks []sessionInfoBlock) []string {
53	if len(blocks) == 0 {
54		return rows
55	}
56
57	out := make([]string, 0, len(rows))
58	rowStart := 0
59	for _, block := range blocks {
60		insertAt := len(rows)
61		if block.afterMessage >= 0 && block.afterMessage < len(anchors) {
62			insertAt = anchors[block.afterMessage].Row
63		}
64		if insertAt < rowStart {
65			insertAt = rowStart
66		}
67		out = append(out, rows[rowStart:insertAt]...)
68		out = append(out, block.lines...)
69		rowStart = insertAt
70	}
71	return append(out, rows[rowStart:]...)
72}
73
74func clampSessionInfoBlocks(blocks []sessionInfoBlock, messageCount int) {
75	for idx := range blocks {
76		if blocks[idx].afterMessage > messageCount {
77			blocks[idx].afterMessage = messageCount
78		}
79	}
80}
81
82func sessionInfoBlocksKey(blocks []sessionInfoBlock) string {
83	var key strings.Builder
84	for _, block := range blocks {
85		fmt.Fprintf(&key, "%d\n", block.afterMessage)
86		for _, line := range block.lines {
87			key.WriteString(line)
88			key.WriteByte('\n')
89		}
90	}
91	return key.String()
92}
93
94func valueOrUnavailable(value string) string {
95	if strings.TrimSpace(value) == "" {
96		return "unavailable"
97	}
98	return value
99}
100
101func absoluteSessionInfoPath(path string) string {
102	if path == "" {
103		return ""
104	}
105	absolute, err := filepath.Abs(path)
106	if err != nil {
107		return path
108	}
109	return absolute
110}
111
112func (i *Interactive) showSessionInfo() {
113	i.mu.Lock()
114	agent := i.agent
115	pathFn := i.cfg.CurrentSessionPath
116	cwd := i.cfg.CWD
117	providerName := i.cfg.Provider
118	model := i.cfg.Model
119	theme := i.cfg.Theme
120	terminal := i.cfg.Terminal
121	i.mu.Unlock()
122
123	sessionID := sessionID(agent)
124	sessionPath := ""
125	if pathFn != nil {
126		sessionPath = pathFn()
127	}
128
129	width := 80
130	if terminal != nil {
131		width, _ = terminal.Size()
132	}
133	block := renderSessionInfoBlock(theme, width, sessionID, absoluteSessionInfoPath(sessionPath), absoluteSessionInfoPath(cwd), providerName, model)
134	messageCount := 0
135	if agent != nil {
136		messageCount = len(filterHiddenTranscriptMessages(agent.Messages()))
137	}
138
139	i.mu.Lock()
140	i.helpBlock = nil
141	i.sessionInfoBlocks = append(i.sessionInfoBlocks, sessionInfoBlock{
142		afterMessage: messageCount,
143		lines:        block,
144	})
145	i.statusErr = ""
146	i.statusOK = ""
147	i.scrollOffset = 0
148	i.mu.Unlock()
149}
150
151func sessionID(agent *core.Agent) string {
152	if agent == nil {
153		return ""
154	}
155	return agent.SessionID()
156}

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 477

↑↓ moveenter openesc close