@@ -40,30 +40,28 @@ const (
40
40
statePrompt
41
41
// stateHelp is the state when a help screen is displayed.
42
42
stateHelp
43
+ // stateConfirm is the state when a confirmation modal is displayed.
44
+ stateConfirm
43
45
)
44
46
45
47
type home struct {
46
48
ctx context.Context
47
49
50
+ // -- Storage and Configuration --
51
+
48
52
program string
49
53
autoYes bool
50
54
51
- // ui components
52
- list * ui.List
53
- menu * ui.Menu
54
- tabbedWindow * ui.TabbedWindow
55
- errBox * ui.ErrBox
56
- // global spinner instance. we plumb this down to where it's needed
57
- spinner spinner.Model
58
-
59
55
// storage is the interface for saving/loading data to/from the app's state
60
56
storage * session.Storage
61
57
// appConfig stores persistent application configuration
62
58
appConfig * config.Config
63
59
// appState stores persistent application state like seen help screens
64
60
appState config.AppState
65
61
66
- // state
62
+ // -- State --
63
+
64
+ // state is the current discrete state of the application
67
65
state state
68
66
// newInstanceFinalizer is called when the state is stateNew and then you press enter.
69
67
// It registers the new instance in the list after the instance has been started.
@@ -72,14 +70,27 @@ type home struct {
72
70
// promptAfterName tracks if we should enter prompt mode after naming
73
71
promptAfterName bool
74
72
75
- // textInputOverlay is the component for handling text input with state
76
- textInputOverlay * overlay.TextInputOverlay
77
-
78
- // textOverlay is the component for displaying text information
79
- textOverlay * overlay.TextOverlay
80
-
81
73
// keySent is used to manage underlining menu items
82
74
keySent bool
75
+
76
+ // -- UI Components --
77
+
78
+ // list displays the list of instances
79
+ list * ui.List
80
+ // menu displays the bottom menu
81
+ menu * ui.Menu
82
+ // tabbedWindow displays the tabbed window with preview and diff panes
83
+ tabbedWindow * ui.TabbedWindow
84
+ // errBox displays error messages
85
+ errBox * ui.ErrBox
86
+ // global spinner instance. we plumb this down to where it's needed
87
+ spinner spinner.Model
88
+ // textInputOverlay handles text input with state
89
+ textInputOverlay * overlay.TextInputOverlay
90
+ // textOverlay displays text information
91
+ textOverlay * overlay.TextOverlay
92
+ // confirmationOverlay displays confirmation modals
93
+ confirmationOverlay * overlay.ConfirmationOverlay
83
94
}
84
95
85
96
func newHome (ctx context.Context , program string , autoYes bool ) * home {
@@ -228,6 +239,12 @@ func (m *home) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
228
239
case tea.WindowSizeMsg :
229
240
m .updateHandleWindowSizeEvent (msg )
230
241
return m , nil
242
+ case error :
243
+ // Handle errors from confirmation actions
244
+ return m , m .handleError (msg )
245
+ case instanceChangedMsg :
246
+ // Handle instance changed after confirmation action
247
+ return m , m .instanceChanged ()
231
248
case spinner.TickMsg :
232
249
var cmd tea.Cmd
233
250
m .spinner , cmd = m .spinner .Update (msg )
@@ -250,7 +267,7 @@ func (m *home) handleMenuHighlighting(msg tea.KeyMsg) (cmd tea.Cmd, returnEarly
250
267
m .keySent = false
251
268
return nil , false
252
269
}
253
- if m .state == statePrompt || m .state == stateHelp {
270
+ if m .state == statePrompt || m .state == stateHelp || m . state == stateConfirm {
254
271
return nil , false
255
272
}
256
273
// If it's in the global keymap, we should try to highlight it.
@@ -405,6 +422,17 @@ func (m *home) handleKeyPress(msg tea.KeyMsg) (mod tea.Model, cmd tea.Cmd) {
405
422
return m , nil
406
423
}
407
424
425
+ // Handle confirmation state
426
+ if m .state == stateConfirm {
427
+ shouldClose := m .confirmationOverlay .HandleKeyPress (msg )
428
+ if shouldClose {
429
+ m .state = stateDefault
430
+ m .confirmationOverlay = nil
431
+ return m , nil
432
+ }
433
+ return m , nil
434
+ }
435
+
408
436
// Handle quit commands first
409
437
if msg .String () == "ctrl+c" || msg .String () == "q" {
410
438
return m .handleQuit ()
@@ -485,45 +513,59 @@ func (m *home) handleKeyPress(msg tea.KeyMsg) (mod tea.Model, cmd tea.Cmd) {
485
513
return m , nil
486
514
}
487
515
488
- worktree , err := selected .GetGitWorktree ()
489
- if err != nil {
490
- return m , m .handleError (err )
491
- }
516
+ // Create the kill action as a tea.Cmd
517
+ killAction := func () tea.Msg {
518
+ // Get worktree and check if branch is checked out
519
+ worktree , err := selected .GetGitWorktree ()
520
+ if err != nil {
521
+ return err
522
+ }
492
523
493
- checkedOut , err := worktree .IsBranchCheckedOut ()
494
- if err != nil {
495
- return m , m . handleError ( err )
496
- }
524
+ checkedOut , err := worktree .IsBranchCheckedOut ()
525
+ if err != nil {
526
+ return err
527
+ }
497
528
498
- if checkedOut {
499
- return m , m . handleError ( fmt .Errorf ("instance %s is currently checked out" , selected .Title ) )
500
- }
529
+ if checkedOut {
530
+ return fmt .Errorf ("instance %s is currently checked out" , selected .Title )
531
+ }
501
532
502
- // Delete from storage first
503
- if err := m .storage .DeleteInstance (selected .Title ); err != nil {
504
- return m , m .handleError (err )
533
+ // Delete from storage first
534
+ if err := m .storage .DeleteInstance (selected .Title ); err != nil {
535
+ return err
536
+ }
537
+
538
+ // Then kill the instance
539
+ m .list .Kill ()
540
+ return instanceChangedMsg {}
505
541
}
506
542
507
- // Then kill the instance
508
- m . list . Kill ( )
509
- return m , m .instanceChanged ( )
543
+ // Show confirmation modal
544
+ message := fmt . Sprintf ( "[!] Kill session '%s'?" , selected . Title )
545
+ return m , m .confirmAction ( message , killAction )
510
546
case keys .KeySubmit :
511
547
selected := m .list .GetSelectedInstance ()
512
548
if selected == nil {
513
549
return m , nil
514
550
}
515
551
516
- // Default commit message with timestamp
517
- commitMsg := fmt .Sprintf ("[claudesquad] update from '%s' on %s" , selected .Title , time .Now ().Format (time .RFC822 ))
518
- worktree , err := selected .GetGitWorktree ()
519
- if err != nil {
520
- return m , m .handleError (err )
521
- }
522
- if err = worktree .PushChanges (commitMsg , true ); err != nil {
523
- return m , m .handleError (err )
552
+ // Create the push action as a tea.Cmd
553
+ pushAction := func () tea.Msg {
554
+ // Default commit message with timestamp
555
+ commitMsg := fmt .Sprintf ("[claudesquad] update from '%s' on %s" , selected .Title , time .Now ().Format (time .RFC822 ))
556
+ worktree , err := selected .GetGitWorktree ()
557
+ if err != nil {
558
+ return err
559
+ }
560
+ if err = worktree .PushChanges (commitMsg , true ); err != nil {
561
+ return err
562
+ }
563
+ return nil
524
564
}
525
565
526
- return m , nil
566
+ // Show confirmation modal
567
+ message := fmt .Sprintf ("[!] Push changes from session '%s'?" , selected .Title )
568
+ return m , m .confirmAction (message , pushAction )
527
569
case keys .KeyCheckout :
528
570
selected := m .list .GetSelectedInstance ()
529
571
if selected == nil {
@@ -611,6 +653,8 @@ type previewTickMsg struct{}
611
653
612
654
type tickUpdateMetadataMessage struct {}
613
655
656
+ type instanceChangedMsg struct {}
657
+
614
658
// tickUpdateMetadataCmd is the callback to update the metadata of the instances every 500ms. Note that we iterate
615
659
// overall the instances and capture their output. It's a pretty expensive operation. Let's do it 2x a second only.
616
660
var tickUpdateMetadataCmd = func () tea.Msg {
@@ -633,6 +677,31 @@ func (m *home) handleError(err error) tea.Cmd {
633
677
}
634
678
}
635
679
680
+ // confirmAction shows a confirmation modal and stores the action to execute on confirm
681
+ func (m * home ) confirmAction (message string , action tea.Cmd ) tea.Cmd {
682
+ m .state = stateConfirm
683
+
684
+ // Create and show the confirmation overlay using ConfirmationOverlay
685
+ m .confirmationOverlay = overlay .NewConfirmationOverlay (message )
686
+ // Set a fixed width for consistent appearance
687
+ m .confirmationOverlay .SetWidth (50 )
688
+
689
+ // Set callbacks for confirmation and cancellation
690
+ m .confirmationOverlay .OnConfirm = func () {
691
+ m .state = stateDefault
692
+ // Execute the action if it exists
693
+ if action != nil {
694
+ _ = action ()
695
+ }
696
+ }
697
+
698
+ m .confirmationOverlay .OnCancel = func () {
699
+ m .state = stateDefault
700
+ }
701
+
702
+ return nil
703
+ }
704
+
636
705
func (m * home ) View () string {
637
706
listWithPadding := lipgloss .NewStyle ().PaddingTop (1 ).Render (m .list .String ())
638
707
previewWithPadding := lipgloss .NewStyle ().PaddingTop (1 ).Render (m .tabbedWindow .String ())
@@ -655,6 +724,11 @@ func (m *home) View() string {
655
724
log .ErrorLog .Printf ("text overlay is nil" )
656
725
}
657
726
return overlay .PlaceOverlay (0 , 0 , m .textOverlay .Render (), mainView , true , true )
727
+ } else if m .state == stateConfirm {
728
+ if m .confirmationOverlay == nil {
729
+ log .ErrorLog .Printf ("confirmation overlay is nil" )
730
+ }
731
+ return overlay .PlaceOverlay (0 , 0 , m .confirmationOverlay .Render (), mainView , true , true )
658
732
}
659
733
660
734
return mainView
0 commit comments