Skip to content

Commit dca886d

Browse files
feat: confirmation modal for D and p (#105)
This change introduces a confirmation dialog when deleting an instance and when pushing changes to GitHub. Fixes #102. --------- Co-authored-by: Mufeez Amjad <[email protected]>
1 parent 946434e commit dca886d

File tree

3 files changed

+665
-42
lines changed

3 files changed

+665
-42
lines changed

app/app.go

Lines changed: 116 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -40,30 +40,28 @@ const (
4040
statePrompt
4141
// stateHelp is the state when a help screen is displayed.
4242
stateHelp
43+
// stateConfirm is the state when a confirmation modal is displayed.
44+
stateConfirm
4345
)
4446

4547
type home struct {
4648
ctx context.Context
4749

50+
// -- Storage and Configuration --
51+
4852
program string
4953
autoYes bool
5054

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-
5955
// storage is the interface for saving/loading data to/from the app's state
6056
storage *session.Storage
6157
// appConfig stores persistent application configuration
6258
appConfig *config.Config
6359
// appState stores persistent application state like seen help screens
6460
appState config.AppState
6561

66-
// state
62+
// -- State --
63+
64+
// state is the current discrete state of the application
6765
state state
6866
// newInstanceFinalizer is called when the state is stateNew and then you press enter.
6967
// It registers the new instance in the list after the instance has been started.
@@ -72,14 +70,27 @@ type home struct {
7270
// promptAfterName tracks if we should enter prompt mode after naming
7371
promptAfterName bool
7472

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-
8173
// keySent is used to manage underlining menu items
8274
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
8394
}
8495

8596
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) {
228239
case tea.WindowSizeMsg:
229240
m.updateHandleWindowSizeEvent(msg)
230241
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()
231248
case spinner.TickMsg:
232249
var cmd tea.Cmd
233250
m.spinner, cmd = m.spinner.Update(msg)
@@ -250,7 +267,7 @@ func (m *home) handleMenuHighlighting(msg tea.KeyMsg) (cmd tea.Cmd, returnEarly
250267
m.keySent = false
251268
return nil, false
252269
}
253-
if m.state == statePrompt || m.state == stateHelp {
270+
if m.state == statePrompt || m.state == stateHelp || m.state == stateConfirm {
254271
return nil, false
255272
}
256273
// 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) {
405422
return m, nil
406423
}
407424

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+
408436
// Handle quit commands first
409437
if msg.String() == "ctrl+c" || msg.String() == "q" {
410438
return m.handleQuit()
@@ -485,45 +513,59 @@ func (m *home) handleKeyPress(msg tea.KeyMsg) (mod tea.Model, cmd tea.Cmd) {
485513
return m, nil
486514
}
487515

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+
}
492523

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+
}
497528

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+
}
501532

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{}
505541
}
506542

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)
510546
case keys.KeySubmit:
511547
selected := m.list.GetSelectedInstance()
512548
if selected == nil {
513549
return m, nil
514550
}
515551

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
524564
}
525565

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)
527569
case keys.KeyCheckout:
528570
selected := m.list.GetSelectedInstance()
529571
if selected == nil {
@@ -611,6 +653,8 @@ type previewTickMsg struct{}
611653

612654
type tickUpdateMetadataMessage struct{}
613655

656+
type instanceChangedMsg struct{}
657+
614658
// tickUpdateMetadataCmd is the callback to update the metadata of the instances every 500ms. Note that we iterate
615659
// overall the instances and capture their output. It's a pretty expensive operation. Let's do it 2x a second only.
616660
var tickUpdateMetadataCmd = func() tea.Msg {
@@ -633,6 +677,31 @@ func (m *home) handleError(err error) tea.Cmd {
633677
}
634678
}
635679

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+
636705
func (m *home) View() string {
637706
listWithPadding := lipgloss.NewStyle().PaddingTop(1).Render(m.list.String())
638707
previewWithPadding := lipgloss.NewStyle().PaddingTop(1).Render(m.tabbedWindow.String())
@@ -655,6 +724,11 @@ func (m *home) View() string {
655724
log.ErrorLog.Printf("text overlay is nil")
656725
}
657726
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)
658732
}
659733

660734
return mainView

0 commit comments

Comments
 (0)