on main help sign in
refactor(ui): rename files.go to session.go and update session loading logic
commite5932974c5cbc41e1dde2624fc38be3e5d96ab3e authorAyman Bagabas <ayman.bagabas@gmail.com> dateTue 16 Dec 2025, 13:36 (1 year ago) parente24c9c3
2 files changed+102−74
internal/ui/model/files.go → internal/ui/model/session.go+34−8 internal/ui/model/ui.go+68−66
internal/ui/model/files.go → internal/ui/model/session.gorenamed+34−8
@@ -12,11 +12,20 @@ import (
1212 "github.com/charmbracelet/crush/internal/diff"
1313 "github.com/charmbracelet/crush/internal/fsext"
1414 "github.com/charmbracelet/crush/internal/history"
15+ "github.com/charmbracelet/crush/internal/session"
1516 "github.com/charmbracelet/crush/internal/ui/common"
1617 "github.com/charmbracelet/crush/internal/ui/styles"
18+ "github.com/charmbracelet/crush/internal/uiutil"
1719 "github.com/charmbracelet/x/ansi"
1820)
1921
22+// loadSessionMsg is a message indicating that a session and its files have
23+// been loaded.
24+type loadSessionMsg struct {
25+ session *session.Session
26+ files []SessionFile
27+}
28+
2029// SessionFile tracks the first and latest versions of a file in a session,
2130// along with the total additions and deletions.
2231type SessionFile struct {
@@ -26,14 +35,24 @@ type SessionFile struct {
2635 Deletions int
2736}
2837
29−// loadSessionFiles loads all files modified during a session and calculates
30−// their diff statistics.
31−func (m *UI) loadSessionFiles(sessionID string) tea.Cmd {
38+// loadSession loads the session along with its associated files and computes
39+// the diff statistics (additions and deletions) for each file in the session.
40+// It returns a tea.Cmd that, when executed, fetches the session data and
41+// returns a sessionFilesLoadedMsg containing the processed session files.
42+func (m *UI) loadSession(sessionID string) tea.Cmd {
3243 return func() tea.Msg {
44+ session, err := m.com.App.Sessions.Get(context.Background(), sessionID)
45+ if err != nil {
46+ // TODO: better error handling
47+ return uiutil.ReportError(err)()
48+ }
49+
3350 files, err := m.com.App.History.ListBySession(context.Background(), sessionID)
3451 if err != nil {
35− return err
52+ // TODO: better error handling
53+ return uiutil.ReportError(err)()
3654 }
55+
3756 filesByPath := make(map[string][]history.File)
3857 for _, f := range files {
3958 filesByPath[f.Path] = append(filesByPath[f.Path], f)
@@ -76,8 +95,9 @@ func (m *UI) loadSessionFiles(sessionID string) tea.Cmd {
7695 return 0
7796 })
7897
79− return sessionFilesLoadedMsg{
80− files: sessionFiles,
98+ return loadSessionMsg{
99+ session: &session,
100+ files: sessionFiles,
81101 }
82102 }
83103}
@@ -108,7 +128,10 @@ func (m *UI) handleFileEvent(file history.File) tea.Cmd {
108128 })
109129 newFiles = append(newFiles, m.sessionFiles...)
110130
111− return sessionFilesLoadedMsg{files: newFiles}
131+ return loadSessionMsg{
132+ session: m.session,
133+ files: newFiles,
134+ }
112135 }
113136
114137 updated := m.sessionFiles[existingIdx]
@@ -137,7 +160,10 @@ func (m *UI) handleFileEvent(file history.File) tea.Cmd {
137160 }
138161 }
139162
140− return sessionFilesLoadedMsg{files: newFiles}
163+ return loadSessionMsg{
164+ session: m.session,
165+ files: newFiles,
166+ }
141167 }
142168}
143169
internal/ui/model/ui.go+68−66
@@ -51,19 +51,11 @@ const (
5151 uiChatCompact
5252)
5353
54−// sessionsLoadedMsg is a message indicating that sessions have been loaded.
55−type sessionsLoadedMsg struct {
54+// listSessionsMsg is a message to list available sessions.
55+type listSessionsMsg struct {
5656 sessions []session.Session
5757}
5858
59−type sessionLoadedMsg struct {
60− sess session.Session
61−}
62−
63−type sessionFilesLoadedMsg struct {
64− files []SessionFile
65−}
66−
6759// UI represents the main user interface model.
6860type UI struct {
6961 com *common.Common
@@ -176,10 +168,6 @@ func (m *UI) Init() tea.Cmd {
176168 return tea.Batch(cmds...)
177169}
178170
179−// sessionLoadedDoneMsg indicates that session loading and message appending is
180−// done.
181−type sessionLoadedDoneMsg struct{}
182−
183171// Update handles updates to the UI model.
184172func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
185173 var cmds []tea.Cmd
@@ -189,16 +177,19 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
189177 if !m.sendProgressBar {
190178 m.sendProgressBar = slices.Contains(msg, "WT_SESSION")
191179 }
192− case sessionsLoadedMsg:
193− sessions := dialog.NewSessions(m.com, msg.sessions...)
194− // TODO: Get. Rid. Of. Magic numbers!
195− sessions.SetSize(min(120, m.width-8), 30)
196− m.dialog.OpenDialog(sessions)
197− case sessionLoadedMsg:
180+ case listSessionsMsg:
181+ if cmd := m.openSessionsDialog(msg.sessions); cmd != nil {
182+ cmds = append(cmds, cmd)
183+ }
184+ case loadSessionMsg:
198185 m.state = uiChat
199− m.session = &msg.sess
200− // Load the last 20 messages from this session.
201− msgs, _ := m.com.App.Messages.List(context.Background(), m.session.ID)
186+ m.session = msg.session
187+ m.sessionFiles = msg.files
188+ msgs, err := m.com.App.Messages.List(context.Background(), m.session.ID)
189+ if err != nil {
190+ cmds = append(cmds, uiutil.ReportError(err))
191+ break
192+ }
202193
203194 // Build tool result map to link tool calls with their results
204195 msgPtrs := make([]*message.Message, len(msgs))
@@ -215,17 +206,8 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
215206
216207 m.chat.SetMessages(items...)
217208
218− // Notify that session loading is done to scroll to bottom. This is
219− // needed because we need to draw the chat list first before we can
220− // scroll to bottom.
221− cmds = append(cmds, func() tea.Msg {
222− return sessionLoadedDoneMsg{}
223− })
224− case sessionLoadedDoneMsg:
225209 m.chat.ScrollToBottom()
226210 m.chat.SelectLast()
227− case sessionFilesLoadedMsg:
228− m.sessionFiles = msg.files
229211 case pubsub.Event[history.File]:
230212 cmds = append(cmds, m.handleFileEvent(msg.Payload))
231213 case pubsub.Event[app.LSPEvent]:
@@ -344,14 +326,6 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
344326 return m, tea.Batch(cmds...)
345327}
346328
347−func (m *UI) loadSession(sessionID string) tea.Cmd {
348− return func() tea.Msg {
349− // TODO: handle error
350− session, _ := m.com.App.Sessions.Get(context.Background(), sessionID)
351− return sessionLoadedMsg{session}
352− }
353−}
354−
355329func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
356330 handleQuitKeys := func(msg tea.KeyPressMsg) bool {
357331 switch {
@@ -374,23 +348,10 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
374348 m.updateLayoutAndSize()
375349 return true
376350 case key.Matches(msg, m.keyMap.Commands):
377− if m.dialog.ContainsDialog(dialog.CommandsID) {
378− // Bring to front
379− m.dialog.BringToFront(dialog.CommandsID)
380− } else {
381− sessionID := ""
382− if m.session != nil {
383− sessionID = m.session.ID
384− }
385− commands, err := dialog.NewCommands(m.com, sessionID)
386− if err != nil {
387− cmds = append(cmds, uiutil.ReportError(err))
388− } else {
389− // TODO: Get. Rid. Of. Magic numbers!
390− commands.SetSize(min(120, m.width-8), 30)
391− m.dialog.OpenDialog(commands)
392− }
351+ if cmd := m.openCommandsDialog(); cmd != nil {
352+ cmds = append(cmds, cmd)
393353 }
354+ return true
394355 case key.Matches(msg, m.keyMap.Models):
395356 // TODO: Implement me
396357 case key.Matches(msg, m.keyMap.Sessions):
@@ -398,13 +359,14 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
398359 // Bring to front
399360 m.dialog.BringToFront(dialog.SessionsID)
400361 } else {
401− cmds = append(cmds, m.loadSessionsCmd)
362+ cmds = append(cmds, m.listSessions)
402363 }
403364 return true
404365 }
405366 return false
406367 }
407368
369+ // Route all messages to dialog if one is open.
408370 if m.dialog.HasDialogs() {
409371 // Always handle quit keys first
410372 if handleQuitKeys(msg) {
@@ -420,19 +382,18 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
420382 // Generic dialog messages
421383 case dialog.CloseMsg:
422384 m.dialog.CloseFrontDialog()
385+
423386 // Session dialog messages
424387 case dialog.SessionSelectedMsg:
425388 m.dialog.CloseDialog(dialog.SessionsID)
426− cmds = append(cmds,
427− m.loadSession(msg.Session.ID),
428− m.loadSessionFiles(msg.Session.ID),
429− )
389+ cmds = append(cmds, m.loadSession(msg.Session.ID))
390+
430391 // Command dialog messages
431392 case dialog.ToggleYoloModeMsg:
432393 m.com.App.Permissions.SetSkipRequests(!m.com.App.Permissions.SkipRequests())
433394 m.dialog.CloseDialog(dialog.CommandsID)
434395 case dialog.SwitchSessionsMsg:
435− cmds = append(cmds, m.loadSessionsCmd)
396+ cmds = append(cmds, m.listSessions)
436397 m.dialog.CloseDialog(dialog.CommandsID)
437398 case dialog.CompactMsg:
438399 err := m.com.App.AgentCoordinator.Summarize(context.Background(), msg.SessionID)
@@ -1039,11 +1000,52 @@ func (m *UI) renderSidebarLogo(width int) {
10391000 m.sidebarLogo = renderLogo(m.com.Styles, true, width)
10401001}
10411002
1042−// loadSessionsCmd loads the list of sessions and returns a command that sends
1043−// a sessionFilesLoadedMsg when done.
1044−func (m *UI) loadSessionsCmd() tea.Msg {
1003+// openCommandsDialog opens the commands dialog.
1004+func (m *UI) openCommandsDialog() tea.Cmd {
1005+ if m.dialog.ContainsDialog(dialog.CommandsID) {
1006+ // Bring to front
1007+ m.dialog.BringToFront(dialog.CommandsID)
1008+ return nil
1009+ }
1010+
1011+ sessionID := ""
1012+ if m.session != nil {
1013+ sessionID = m.session.ID
1014+ }
1015+
1016+ commands, err := dialog.NewCommands(m.com, sessionID)
1017+ if err != nil {
1018+ return uiutil.ReportError(err)
1019+ }
1020+
1021+ // TODO: Get. Rid. Of. Magic numbers!
1022+ commands.SetSize(min(120, m.width-8), 30)
1023+ m.dialog.OpenDialog(commands)
1024+
1025+ return nil
1026+}
1027+
1028+// openSessionsDialog opens the sessions dialog with the given sessions.
1029+func (m *UI) openSessionsDialog(sessions []session.Session) tea.Cmd {
1030+ if m.dialog.ContainsDialog(dialog.SessionsID) {
1031+ // Bring to front
1032+ m.dialog.BringToFront(dialog.SessionsID)
1033+ return nil
1034+ }
1035+
1036+ dialog := dialog.NewSessions(m.com, sessions...)
1037+ // TODO: Get. Rid. Of. Magic numbers!
1038+ dialog.SetSize(min(120, m.width-8), 30)
1039+ m.dialog.OpenDialog(dialog)
1040+
1041+ return nil
1042+}
1043+
1044+// listSessions is a [tea.Cmd] that lists all sessions and returns them in a
1045+// [listSessionsMsg].
1046+func (m *UI) listSessions() tea.Msg {
10451047 allSessions, _ := m.com.App.Sessions.List(context.TODO())
1046− return sessionsLoadedMsg{sessions: allSessions}
1048+ return listSessionsMsg{sessions: allSessions}
10471049}
10481050
10491051// renderLogo renders the Crush logo with the given styles and dimensions.