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}