fix(ui): use setState method to change UI state and focus (#1994)

Ayman Bagabas created

This change introduces a setState method in the UI model to encapsulate
the logic for changing the UI state and focus. This ensures that any
time the state or focus is changed, the layout and size are updated
accordingly.

Change summary

internal/ui/model/onboarding.go |  3 -
internal/ui/model/ui.go         | 63 +++++++++++++++++-----------------
2 files changed, 33 insertions(+), 33 deletions(-)

Detailed changes

internal/ui/model/onboarding.go 🔗

@@ -68,8 +68,7 @@ func (m *UI) initializeProject() tea.Cmd {
 // skipInitializeProject skips project initialization and transitions to the landing view.
 func (m *UI) skipInitializeProject() tea.Cmd {
 	// TODO: initialize the project
-	m.state = uiLanding
-	m.focus = uiFocusEditor
+	m.setState(uiLanding, uiFocusEditor)
 	// mark the project as initialized
 	return m.markProjectInitialized
 }

internal/ui/model/ui.go 🔗

@@ -258,8 +258,6 @@ func New(com *common.Common) *UI {
 		com:         com,
 		dialog:      dialog.NewOverlay(),
 		keyMap:      keyMap,
-		focus:       uiFocusNone,
-		state:       uiOnboarding,
 		textarea:    ta,
 		chat:        ch,
 		completions: comp,
@@ -271,18 +269,6 @@ func New(com *common.Common) *UI {
 
 	status := NewStatus(com, ui)
 
-	// set onboarding state defaults
-	ui.onboarding.yesInitializeSelected = true
-
-	if !com.Config().IsConfigured() {
-		ui.state = uiOnboarding
-	} else if n, _ := config.ProjectNeedsInitialization(); n {
-		ui.state = uiInitialize
-	} else {
-		ui.state = uiLanding
-		ui.focus = uiFocusEditor
-	}
-
 	ui.setEditorPrompt(false)
 	ui.randomizePlaceholders()
 	ui.textarea.Placeholder = ui.readyPlaceholder
@@ -291,6 +277,20 @@ func New(com *common.Common) *UI {
 	// Initialize compact mode from config
 	ui.forceCompactMode = com.Config().Options.TUI.CompactMode
 
+	// set onboarding state defaults
+	ui.onboarding.yesInitializeSelected = true
+
+	desiredState := uiLanding
+	desiredFocus := uiFocusEditor
+	if !com.Config().IsConfigured() {
+		desiredState = uiOnboarding
+	} else if n, _ := config.ProjectNeedsInitialization(); n {
+		desiredState = uiInitialize
+	}
+
+	// set initial state
+	ui.setState(desiredState, desiredFocus)
+
 	return ui
 }
 
@@ -310,6 +310,14 @@ func (m *UI) Init() tea.Cmd {
 	return tea.Batch(cmds...)
 }
 
+// setState changes the UI state and focus.
+func (m *UI) setState(state uiState, focus uiFocusState) {
+	m.state = state
+	m.focus = focus
+	// Changing the state may change layout, so update it.
+	m.updateLayoutAndSize()
+}
+
 // loadCustomCommands loads the custom commands asynchronously.
 func (m *UI) loadCustomCommands() tea.Cmd {
 	return func() tea.Msg {
@@ -360,10 +368,10 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			cmds = append(cmds, timage.RequestCapabilities(m.imgCaps.Env))
 		}
 	case loadSessionMsg:
-		m.state = uiChat
 		if m.forceCompactMode {
 			m.isCompact = true
 		}
+		m.setState(uiChat, m.focus)
 		m.session = msg.session
 		m.sessionFiles = msg.files
 		msgs, err := m.com.App.Messages.List(context.Background(), m.session.ID)
@@ -493,7 +501,6 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		return m, nil
 	case tea.WindowSizeMsg:
 		m.width, m.height = msg.Width, msg.Height
-		m.handleCompactMode(m.width, m.height)
 		m.updateLayoutAndSize()
 		// XXX: We need to store cell dimensions for image rendering.
 		m.imgCaps.Columns, m.imgCaps.Rows = msg.Width, msg.Height
@@ -1212,9 +1219,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
 		m.dialog.CloseDialog(dialog.ModelsID)
 
 		if isOnboarding {
-			m.state = uiLanding
-			m.focus = uiFocusEditor
-
+			m.setState(uiLanding, uiFocusEditor)
 			m.com.Config().SetupAgents()
 			if err := m.com.App.InitCoderAgent(context.TODO()); err != nil {
 				cmds = append(cmds, uiutil.ReportError(err))
@@ -1507,7 +1512,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
 				m.newSession()
 			case key.Matches(msg, m.keyMap.Tab):
 				if m.state != uiLanding {
-					m.focus = uiFocusMain
+					m.setState(m.state, uiFocusMain)
 					m.textarea.Blur()
 					m.chat.Focus()
 					m.chat.SetSelected(m.chat.Len() - 1)
@@ -2054,29 +2059,26 @@ func (m *UI) toggleCompactMode() tea.Cmd {
 		return uiutil.ReportError(err)
 	}
 
-	m.handleCompactMode(m.width, m.height)
 	m.updateLayoutAndSize()
 
 	return nil
 }
 
-// handleCompactMode updates the UI state based on window size and compact mode setting.
-func (m *UI) handleCompactMode(newWidth, newHeight int) {
+// updateLayoutAndSize updates the layout and sizes of UI components.
+func (m *UI) updateLayoutAndSize() {
+	// Determine if we should be in compact mode
 	if m.state == uiChat {
 		if m.forceCompactMode {
 			m.isCompact = true
 			return
 		}
-		if newWidth < compactModeWidthBreakpoint || newHeight < compactModeHeightBreakpoint {
+		if m.width < compactModeWidthBreakpoint || m.height < compactModeHeightBreakpoint {
 			m.isCompact = true
 		} else {
 			m.isCompact = false
 		}
 	}
-}
 
-// updateLayoutAndSize updates the layout and sizes of UI components.
-func (m *UI) updateLayoutAndSize() {
 	m.layout = m.generateLayout(m.width, m.height)
 	m.updateSize()
 }
@@ -2121,7 +2123,7 @@ func (m *UI) generateLayout(w, h int) layout {
 	const landingHeaderHeight = 4
 
 	var helpKeyMap help.KeyMap = m
-	if m.status.ShowingAll() {
+	if m.status != nil && m.status.ShowingAll() {
 		for _, row := range helpKeyMap.FullHelp() {
 			helpHeight = max(helpHeight, len(row))
 		}
@@ -2527,7 +2529,6 @@ func (m *UI) sendMessage(content string, attachments ...message.Attachment) tea.
 		if err != nil {
 			return uiutil.ReportError(err)
 		}
-		m.state = uiChat
 		if m.forceCompactMode {
 			m.isCompact = true
 		}
@@ -2535,6 +2536,7 @@ func (m *UI) sendMessage(content string, attachments ...message.Attachment) tea.
 			m.session = &newSession
 			cmds = append(cmds, m.loadSession(newSession.ID))
 		}
+		m.setState(uiChat, m.focus)
 	}
 
 	// Capture session ID to avoid race with main goroutine updating m.session.
@@ -2782,8 +2784,7 @@ func (m *UI) newSession() {
 
 	m.session = nil
 	m.sessionFiles = nil
-	m.state = uiLanding
-	m.focus = uiFocusEditor
+	m.setState(uiLanding, uiFocusEditor)
 	m.textarea.Focus()
 	m.chat.Blur()
 	m.chat.ClearMessages()