66
77 "slices"
88
9+ "github.com/charmbracelet/bubbles/v2/textinput"
910 tea "github.com/charmbracelet/bubbletea/v2"
1011 "github.com/muesli/reflow/truncate"
1112 "github.com/sst/opencode-sdk-go"
@@ -110,6 +111,9 @@ type sessionDialog struct {
110111 list list.List [sessionItem ]
111112 app * app.App
112113 deleteConfirmation int // -1 means no confirmation, >= 0 means confirming deletion of session at this index
114+ renameMode bool
115+ renameInput textinput.Model
116+ renameIndex int // index of session being renamed
113117}
114118
115119func (s * sessionDialog ) Init () tea.Cmd {
@@ -123,69 +127,128 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
123127 s .height = msg .Height
124128 s .list .SetMaxWidth (layout .Current .Container .Width - 12 )
125129 case tea.KeyPressMsg :
126- switch msg .String () {
127- case "enter" :
128- if s .deleteConfirmation >= 0 {
129- s .deleteConfirmation = - 1
130+ if s .renameMode {
131+ switch msg .String () {
132+ case "enter" :
133+ if _ , idx := s .list .GetSelectedItem (); idx >= 0 && idx < len (s .sessions ) && idx == s .renameIndex {
134+ newTitle := s .renameInput .Value ()
135+ if strings .TrimSpace (newTitle ) != "" {
136+ sessionToUpdate := s .sessions [idx ]
137+ return s , tea .Sequence (
138+ func () tea.Msg {
139+ ctx := context .Background ()
140+ err := s .app .UpdateSession (ctx , sessionToUpdate .ID , newTitle )
141+ if err != nil {
142+ return toast .NewErrorToast ("Failed to rename session: " + err .Error ())()
143+ }
144+ s .sessions [idx ].Title = newTitle
145+ s .renameMode = false
146+ s .modal .SetTitle ("Switch Session" )
147+ s .updateListItems ()
148+ return toast .NewSuccessToast ("Session renamed successfully" )()
149+ },
150+ )
151+ }
152+ }
153+ s .renameMode = false
154+ s .modal .SetTitle ("Switch Session" )
130155 s .updateListItems ()
131156 return s , nil
157+ default :
158+ var cmd tea.Cmd
159+ s .renameInput , cmd = s .renameInput .Update (msg )
160+ return s , cmd
132161 }
133- if _ , idx := s .list .GetSelectedItem (); idx >= 0 && idx < len (s .sessions ) {
134- selectedSession := s .sessions [idx ]
162+ } else {
163+ switch msg .String () {
164+ case "enter" :
165+ if s .deleteConfirmation >= 0 {
166+ s .deleteConfirmation = - 1
167+ s .updateListItems ()
168+ return s , nil
169+ }
170+ if _ , idx := s .list .GetSelectedItem (); idx >= 0 && idx < len (s .sessions ) {
171+ selectedSession := s .sessions [idx ]
172+ return s , tea .Sequence (
173+ util .CmdHandler (modal.CloseModalMsg {}),
174+ util .CmdHandler (app .SessionSelectedMsg (& selectedSession )),
175+ )
176+ }
177+ case "n" :
135178 return s , tea .Sequence (
136179 util .CmdHandler (modal.CloseModalMsg {}),
137- util .CmdHandler (app .SessionSelectedMsg ( & selectedSession ) ),
180+ util .CmdHandler (app.SessionClearedMsg {} ),
138181 )
139- }
140- case "n" :
141- return s , tea .Sequence (
142- util .CmdHandler (modal.CloseModalMsg {}),
143- util .CmdHandler (app.SessionClearedMsg {}),
144- )
145- case "x" , "delete" , "backspace" :
146- if _ , idx := s .list .GetSelectedItem (); idx >= 0 && idx < len (s .sessions ) {
147- if s .deleteConfirmation == idx {
148- // Second press - actually delete the session
149- sessionToDelete := s .sessions [idx ]
150- return s , tea .Sequence (
151- func () tea.Msg {
152- s .sessions = slices .Delete (s .sessions , idx , idx + 1 )
153- s .deleteConfirmation = - 1
154- s .updateListItems ()
155- return nil
156- },
157- s .deleteSession (sessionToDelete .ID ),
158- )
159- } else {
160- // First press - enter delete confirmation mode
161- s .deleteConfirmation = idx
182+ case "r" :
183+ if _ , idx := s .list .GetSelectedItem (); idx >= 0 && idx < len (s .sessions ) {
184+ s .renameMode = true
185+ s .renameIndex = idx
186+ s .setupRenameInput (s .sessions [idx ].Title )
187+ s .modal .SetTitle ("Rename Session" )
188+ s .updateListItems ()
189+ return s , textinput .Blink
190+ }
191+ case "x" , "delete" , "backspace" :
192+ if _ , idx := s .list .GetSelectedItem (); idx >= 0 && idx < len (s .sessions ) {
193+ if s .deleteConfirmation == idx {
194+ // Second press - actually delete the session
195+ sessionToDelete := s .sessions [idx ]
196+ return s , tea .Sequence (
197+ func () tea.Msg {
198+ s .sessions = slices .Delete (s .sessions , idx , idx + 1 )
199+ s .deleteConfirmation = - 1
200+ s .updateListItems ()
201+ return nil
202+ },
203+ s .deleteSession (sessionToDelete .ID ),
204+ )
205+ } else {
206+ // First press - enter delete confirmation mode
207+ s .deleteConfirmation = idx
208+ s .updateListItems ()
209+ return s , nil
210+ }
211+ }
212+ case "esc" :
213+ if s .deleteConfirmation >= 0 {
214+ s .deleteConfirmation = - 1
162215 s .updateListItems ()
163216 return s , nil
164217 }
165218 }
166- case "esc" :
167- if s .deleteConfirmation >= 0 {
168- s .deleteConfirmation = - 1
169- s .updateListItems ()
170- return s , nil
171- }
172219 }
173220 }
174221
175- var cmd tea.Cmd
176- listModel , cmd := s .list .Update (msg )
177- s .list = listModel .(list.List [sessionItem ])
178- return s , cmd
222+ if ! s .renameMode {
223+ var cmd tea.Cmd
224+ listModel , cmd := s .list .Update (msg )
225+ s .list = listModel .(list.List [sessionItem ])
226+ return s , cmd
227+ }
228+ return s , nil
179229}
180230
181231func (s * sessionDialog ) Render (background string ) string {
232+ if s .renameMode {
233+ // Show rename input instead of list
234+ t := theme .CurrentTheme ()
235+ renameView := s .renameInput .View ()
236+
237+ mutedStyle := styles .NewStyle ().Foreground (t .TextMuted ()).Background (t .BackgroundPanel ()).Render
238+ helpText := mutedStyle ("Enter to confirm, Esc to cancel" )
239+ helpText = styles .NewStyle ().PaddingLeft (1 ).PaddingTop (1 ).Render (helpText )
240+
241+ content := strings .Join ([]string {renameView , helpText }, "\n " )
242+ return s .modal .Render (content , background )
243+ }
244+
182245 listView := s .list .View ()
183246
184247 t := theme .CurrentTheme ()
185248 keyStyle := styles .NewStyle ().Foreground (t .Text ()).Background (t .BackgroundPanel ()).Render
186249 mutedStyle := styles .NewStyle ().Foreground (t .TextMuted ()).Background (t .BackgroundPanel ()).Render
187250
188- leftHelp := keyStyle ("n" ) + mutedStyle (" new session" )
251+ leftHelp := keyStyle ("n" ) + mutedStyle (" new session" ) + " " + keyStyle ( "r" ) + mutedStyle ( " rename" )
189252 rightHelp := keyStyle ("x/del" ) + mutedStyle (" delete session" )
190253
191254 bgColor := t .BackgroundPanel ()
@@ -203,6 +266,39 @@ func (s *sessionDialog) Render(background string) string {
203266 return s .modal .Render (content , background )
204267}
205268
269+ func (s * sessionDialog ) setupRenameInput (currentTitle string ) {
270+ t := theme .CurrentTheme ()
271+ bgColor := t .BackgroundPanel ()
272+ textColor := t .Text ()
273+ textMutedColor := t .TextMuted ()
274+
275+ s .renameInput = textinput .New ()
276+ s .renameInput .SetValue (currentTitle )
277+ s .renameInput .Focus ()
278+ s .renameInput .CharLimit = 100
279+ s .renameInput .SetWidth (layout .Current .Container .Width - 20 )
280+
281+ s .renameInput .Styles .Blurred .Placeholder = styles .NewStyle ().
282+ Foreground (textMutedColor ).
283+ Background (bgColor ).
284+ Lipgloss ()
285+ s .renameInput .Styles .Blurred .Text = styles .NewStyle ().
286+ Foreground (textColor ).
287+ Background (bgColor ).
288+ Lipgloss ()
289+ s .renameInput .Styles .Focused .Placeholder = styles .NewStyle ().
290+ Foreground (textMutedColor ).
291+ Background (bgColor ).
292+ Lipgloss ()
293+ s .renameInput .Styles .Focused .Text = styles .NewStyle ().
294+ Foreground (textColor ).
295+ Background (bgColor ).
296+ Lipgloss ()
297+ s .renameInput .Styles .Focused .Prompt = styles .NewStyle ().
298+ Background (bgColor ).
299+ Lipgloss ()
300+ }
301+
206302func (s * sessionDialog ) updateListItems () {
207303 _ , currentIdx := s .list .GetSelectedItem ()
208304
@@ -229,7 +325,22 @@ func (s *sessionDialog) deleteSession(sessionID string) tea.Cmd {
229325 }
230326}
231327
328+ // ReopenSessionModalMsg is emitted when the session modal should be reopened
329+ type ReopenSessionModalMsg struct {}
330+
232331func (s * sessionDialog ) Close () tea.Cmd {
332+ if s .renameMode {
333+ // If in rename mode, exit rename mode and return a command to reopen the modal
334+ s .renameMode = false
335+ s .modal .SetTitle ("Switch Session" )
336+ s .updateListItems ()
337+
338+ // Return a command that will reopen the session modal
339+ return func () tea.Msg {
340+ return ReopenSessionModalMsg {}
341+ }
342+ }
343+ // Normal close behavior
233344 return nil
234345}
235346
@@ -272,6 +383,8 @@ func NewSessionDialog(app *app.App) SessionDialog {
272383 list : listComponent ,
273384 app : app ,
274385 deleteConfirmation : - 1 ,
386+ renameMode : false ,
387+ renameIndex : - 1 ,
275388 modal : modal .New (
276389 modal .WithTitle ("Switch Session" ),
277390 modal .WithMaxWidth (layout .Current .Container .Width - 8 ),
0 commit comments