on main help sign in
refactor(ui): rename files.go to session.go and update session loading logic
e593297Ayman Bagabas
internal/ui/model/files.go → internal/ui/model/session.gorenamed+34−8
@@ -12,11 +12,20 @@ import (
12 "github.com/charmbracelet/crush/internal/diff"
12 "github.com/charmbracelet/crush/internal/diff"
13 "github.com/charmbracelet/crush/internal/fsext"
13 "github.com/charmbracelet/crush/internal/fsext"
14 "github.com/charmbracelet/crush/internal/history"
14 "github.com/charmbracelet/crush/internal/history"
15+ "github.com/charmbracelet/crush/internal/session"
15 "github.com/charmbracelet/crush/internal/ui/common"
16 "github.com/charmbracelet/crush/internal/ui/common"
16 "github.com/charmbracelet/crush/internal/ui/styles"
17 "github.com/charmbracelet/crush/internal/ui/styles"
18+ "github.com/charmbracelet/crush/internal/uiutil"
17 "github.com/charmbracelet/x/ansi"
19 "github.com/charmbracelet/x/ansi"
18)
20)
19
21
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+
20// SessionFile tracks the first and latest versions of a file in a session,
29// SessionFile tracks the first and latest versions of a file in a session,
21// along with the total additions and deletions.
30// along with the total additions and deletions.
22type SessionFile struct {
31type SessionFile struct {
@@ -26,14 +35,24 @@ type SessionFile struct {
26 Deletions int
35 Deletions int
27}
36}
28
37
29−// loadSessionFiles loads all files modified during a session and calculates
38+// loadSession loads the session along with its associated files and computes
30−// their diff statistics.
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.
31−func (m *UI) loadSessionFiles(sessionID string) tea.Cmd {
42+func (m *UI) loadSession(sessionID string) tea.Cmd {
32 return func() tea.Msg {
43 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+
33 files, err := m.com.App.History.ListBySession(context.Background(), sessionID)
50 files, err := m.com.App.History.ListBySession(context.Background(), sessionID)
34 if err != nil {
51 if err != nil {
52+ // TODO: better error handling
35− return err
53+ return uiutil.ReportError(err)()
36 }
54 }
55+
37 filesByPath := make(map[string][]history.File)
56 filesByPath := make(map[string][]history.File)
38 for _, f := range files {
57 for _, f := range files {
39 filesByPath[f.Path] = append(filesByPath[f.Path], f)
58 filesByPath[f.Path] = append(filesByPath[f.Path], f)
@@ -76,8 +95,9 @@ func (m *UI) loadSessionFiles(sessionID string) tea.Cmd {
76 return 0
95 return 0
77 })
96 })
78
97
79− return sessionFilesLoadedMsg{
98+ return loadSessionMsg{
99+ session: &session,
80− files: sessionFiles,
100+ files: sessionFiles,
81 }
101 }
82 }
102 }
83}
103}
@@ -108,7 +128,10 @@ func (m *UI) handleFileEvent(file history.File) tea.Cmd {
108 })
128 })
109 newFiles = append(newFiles, m.sessionFiles...)
129 newFiles = append(newFiles, m.sessionFiles...)
110
130
131+ return loadSessionMsg{
132+ session: m.session,
111− return sessionFilesLoadedMsg{files: newFiles}
133+ files: newFiles,
134+ }
112 }
135 }
113
136
114 updated := m.sessionFiles[existingIdx]
137 updated := m.sessionFiles[existingIdx]
@@ -137,7 +160,10 @@ func (m *UI) handleFileEvent(file history.File) tea.Cmd {
137 }
160 }
138 }
161 }
139
162
163+ return loadSessionMsg{
164+ session: m.session,
140− return sessionFilesLoadedMsg{files: newFiles}
165+ files: newFiles,
166+ }
141 }
167 }
142}
168}
143
169
internal/ui/model/ui.go+68−66
@@ -51,19 +51,11 @@ const (
51 uiChatCompact
51 uiChatCompact
52)
52)
53
53
54−// sessionsLoadedMsg is a message indicating that sessions have been loaded.
54+// listSessionsMsg is a message to list available sessions.
55−type sessionsLoadedMsg struct {
55+type listSessionsMsg struct {
56 sessions []session.Session
56 sessions []session.Session
57}
57}
58
58
59−type sessionLoadedMsg struct {
60− sess session.Session
61−}
62−
63−type sessionFilesLoadedMsg struct {
64− files []SessionFile
65−}
66−
67// UI represents the main user interface model.
59// UI represents the main user interface model.
68type UI struct {
60type UI struct {
69 com *common.Common
61 com *common.Common
@@ -176,10 +168,6 @@ func (m *UI) Init() tea.Cmd {
176 return tea.Batch(cmds...)
168 return tea.Batch(cmds...)
177}
169}
178
170
179−// sessionLoadedDoneMsg indicates that session loading and message appending is
180−// done.
181−type sessionLoadedDoneMsg struct{}
182−
183// Update handles updates to the UI model.
171// Update handles updates to the UI model.
184func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
172func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
185 var cmds []tea.Cmd
173 var cmds []tea.Cmd
@@ -189,16 +177,19 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
189 if !m.sendProgressBar {
177 if !m.sendProgressBar {
190 m.sendProgressBar = slices.Contains(msg, "WT_SESSION")
178 m.sendProgressBar = slices.Contains(msg, "WT_SESSION")
191 }
179 }
192− case sessionsLoadedMsg:
180+ case listSessionsMsg:
193− sessions := dialog.NewSessions(m.com, msg.sessions...)
181+ if cmd := m.openSessionsDialog(msg.sessions); cmd != nil {
194− // TODO: Get. Rid. Of. Magic numbers!
182+ cmds = append(cmds, cmd)
195− sessions.SetSize(min(120, m.width-8), 30)
183+ }
196− m.dialog.OpenDialog(sessions)
184+ case loadSessionMsg:
197− case sessionLoadedMsg:
198 m.state = uiChat
185 m.state = uiChat
199− m.session = &msg.sess
186+ m.session = msg.session
200− // Load the last 20 messages from this session.
187+ m.sessionFiles = msg.files
201− msgs, _ := m.com.App.Messages.List(context.Background(), m.session.ID)
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+ }
202
193
203 // Build tool result map to link tool calls with their results
194 // Build tool result map to link tool calls with their results
204 msgPtrs := make([]*message.Message, len(msgs))
195 msgPtrs := make([]*message.Message, len(msgs))
@@ -215,17 +206,8 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
215
206
216 m.chat.SetMessages(items...)
207 m.chat.SetMessages(items...)
217
208
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:
225 m.chat.ScrollToBottom()
209 m.chat.ScrollToBottom()
226 m.chat.SelectLast()
210 m.chat.SelectLast()
227− case sessionFilesLoadedMsg:
228− m.sessionFiles = msg.files
229 case pubsub.Event[history.File]:
211 case pubsub.Event[history.File]:
230 cmds = append(cmds, m.handleFileEvent(msg.Payload))
212 cmds = append(cmds, m.handleFileEvent(msg.Payload))
231 case pubsub.Event[app.LSPEvent]:
213 case pubsub.Event[app.LSPEvent]:
@@ -344,14 +326,6 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
344 return m, tea.Batch(cmds...)
326 return m, tea.Batch(cmds...)
345}
327}
346
328
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−
355func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
329func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
356 handleQuitKeys := func(msg tea.KeyPressMsg) bool {
330 handleQuitKeys := func(msg tea.KeyPressMsg) bool {
357 switch {
331 switch {
@@ -374,23 +348,10 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
374 m.updateLayoutAndSize()
348 m.updateLayoutAndSize()
375 return true
349 return true
376 case key.Matches(msg, m.keyMap.Commands):
350 case key.Matches(msg, m.keyMap.Commands):
377− if m.dialog.ContainsDialog(dialog.CommandsID) {
351+ if cmd := m.openCommandsDialog(); cmd != nil {
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))
352+ cmds = append(cmds, cmd)
388− } else {
389− // TODO: Get. Rid. Of. Magic numbers!
390− commands.SetSize(min(120, m.width-8), 30)
391− m.dialog.OpenDialog(commands)
392− }
393 }
353 }
354+ return true
394 case key.Matches(msg, m.keyMap.Models):
355 case key.Matches(msg, m.keyMap.Models):
395 // TODO: Implement me
356 // TODO: Implement me
396 case key.Matches(msg, m.keyMap.Sessions):
357 case key.Matches(msg, m.keyMap.Sessions):
@@ -398,13 +359,14 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
398 // Bring to front
359 // Bring to front
399 m.dialog.BringToFront(dialog.SessionsID)
360 m.dialog.BringToFront(dialog.SessionsID)
400 } else {
361 } else {
401− cmds = append(cmds, m.loadSessionsCmd)
362+ cmds = append(cmds, m.listSessions)
402 }
363 }
403 return true
364 return true
404 }
365 }
405 return false
366 return false
406 }
367 }
407
368
369+ // Route all messages to dialog if one is open.
408 if m.dialog.HasDialogs() {
370 if m.dialog.HasDialogs() {
409 // Always handle quit keys first
371 // Always handle quit keys first
410 if handleQuitKeys(msg) {
372 if handleQuitKeys(msg) {
@@ -420,19 +382,18 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
420 // Generic dialog messages
382 // Generic dialog messages
421 case dialog.CloseMsg:
383 case dialog.CloseMsg:
422 m.dialog.CloseFrontDialog()
384 m.dialog.CloseFrontDialog()
385+
423 // Session dialog messages
386 // Session dialog messages
424 case dialog.SessionSelectedMsg:
387 case dialog.SessionSelectedMsg:
425 m.dialog.CloseDialog(dialog.SessionsID)
388 m.dialog.CloseDialog(dialog.SessionsID)
426− cmds = append(cmds,
427− m.loadSession(msg.Session.ID),
389+ cmds = append(cmds, m.loadSession(msg.Session.ID))
428− m.loadSessionFiles(msg.Session.ID),
390+
429− )
430 // Command dialog messages
391 // Command dialog messages
431 case dialog.ToggleYoloModeMsg:
392 case dialog.ToggleYoloModeMsg:
432 m.com.App.Permissions.SetSkipRequests(!m.com.App.Permissions.SkipRequests())
393 m.com.App.Permissions.SetSkipRequests(!m.com.App.Permissions.SkipRequests())
433 m.dialog.CloseDialog(dialog.CommandsID)
394 m.dialog.CloseDialog(dialog.CommandsID)
434 case dialog.SwitchSessionsMsg:
395 case dialog.SwitchSessionsMsg:
435− cmds = append(cmds, m.loadSessionsCmd)
396+ cmds = append(cmds, m.listSessions)
436 m.dialog.CloseDialog(dialog.CommandsID)
397 m.dialog.CloseDialog(dialog.CommandsID)
437 case dialog.CompactMsg:
398 case dialog.CompactMsg:
438 err := m.com.App.AgentCoordinator.Summarize(context.Background(), msg.SessionID)
399 err := m.com.App.AgentCoordinator.Summarize(context.Background(), msg.SessionID)
@@ -1039,11 +1000,52 @@ func (m *UI) renderSidebarLogo(width int) {
1039 m.sidebarLogo = renderLogo(m.com.Styles, true, width)
1000 m.sidebarLogo = renderLogo(m.com.Styles, true, width)
1040}
1001}
1041
1002
1042−// loadSessionsCmd loads the list of sessions and returns a command that sends
1003+// openCommandsDialog opens the commands dialog.
1043−// a sessionFilesLoadedMsg when done.
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].
1044−func (m *UI) loadSessionsCmd() tea.Msg {
1046+func (m *UI) listSessions() tea.Msg {
1045 allSessions, _ := m.com.App.Sessions.List(context.TODO())
1047 allSessions, _ := m.com.App.Sessions.List(context.TODO())
1046− return sessionsLoadedMsg{sessions: allSessions}
1048+ return listSessionsMsg{sessions: allSessions}
1047}
1049}
1048
1050
1049// renderLogo renders the Crush logo with the given styles and dimensions.
1051// renderLogo renders the Crush logo with the given styles and dimensions.