on main help sign in
refactor: replace magic numbers with named constants

- Add layout constants for borders, padding, and positioning - Add timing constant for cancel timer duration - Improve code readability and maintainability - Make layout calculations self-documenting

commitbc1992fae831058179c155327f538e563a8ca32c authorKujtim Hoxha <kujtimii.h@gmail.com> dateTue 8 Jul 2025, 17:39 (1 year ago) parent8dff4d6
1 file changed+101−107
internal/tui/page/chat/chat.go+101−107
internal/tui/page/chat/chat.go+101−107
@@ -35,20 +35,11 @@ var ChatPageID page.PageID = "chat"
3535type (
3636 OpenFilePickerMsg struct{}
3737 ChatFocusedMsg struct {
38− Focused bool // True if the chat input is focused, false otherwise
38+ Focused bool
3939 }
4040 CancelTimerExpiredMsg struct{}
4141)
4242
43−type ChatState string
44−
45−const (
46− ChatStateOnboarding ChatState = "onboarding"
47− ChatStateInitProject ChatState = "init_project"
48− ChatStateNewMessage ChatState = "new_message"
49− ChatStateInSession ChatState = "in_session"
50−)
51−
5243type PanelType string
5344
5445const (
@@ -63,6 +54,15 @@ const (
6354 SideBarWidth = 31 // Width of the sidebar
6455 SideBarDetailsPadding = 1 // Padding for the sidebar details section
6556 HeaderHeight = 1 // Height of the header
57+
58+ // Layout constants for borders and padding
59+ BorderWidth = 1 // Width of component borders
60+ LeftRightBorders = 2 // Left + right border width (1 + 1)
61+ TopBottomBorders = 2 // Top + bottom border width (1 + 1)
62+ DetailsPositioning = 2 // Positioning adjustment for details panel
63+
64+ // Timing constants
65+ CancelTimerDuration = 2 * time.Second // Duration before cancel timer expires
6666)
6767
6868type ChatPage interface {
@@ -70,9 +70,9 @@ type ChatPage interface {
7070 layout.Help
7171}
7272
73−// cancelTimerCmd creates a command that expires the cancel timer after 2 seconds
73+// cancelTimerCmd creates a command that expires the cancel timer
7474func cancelTimerCmd() tea.Cmd {
75− return tea.Tick(2*time.Second, func(time.Time) tea.Msg {
75+ return tea.Tick(CancelTimerDuration, func(time.Time) tea.Msg {
7676 return CancelTimerExpiredMsg{}
7777 })
7878}
@@ -81,34 +81,33 @@ type chatPage struct {
8181 width, height int
8282 detailsWidth, detailsHeight int
8383 app *app.App
84− state ChatState
85− session session.Session
86− keyMap KeyMap
87− focusedPane PanelType
88− // Compact mode
89− compact bool
90− header header.Header
91− showingDetails bool
92−
93− sidebar sidebar.Sidebar
94− chat chat.MessageListCmp
95− editor editor.Editor
96− splash splash.Splash
97− canceling bool
98−
99− // This will force the compact mode even in big screens
100− // usually triggered by the user command
101− // this will also be set when the user config is set to compact mode
84+
85+ // Layout state
86+ compact bool
10287 forceCompact bool
88+ focusedPane PanelType
89+
90+ // Session
91+ session session.Session
92+ keyMap KeyMap
93+
94+ // Components
95+ header header.Header
96+ sidebar sidebar.Sidebar
97+ chat chat.MessageListCmp
98+ editor editor.Editor
99+ splash splash.Splash
100+
101+ // Simple state flags
102+ showingDetails bool
103+ isCanceling bool
104+ splashFullScreen bool
103105}
104106
105107func New(app *app.App) ChatPage {
106108 return &chatPage{
107− app: app,
108− state: ChatStateOnboarding,
109−
110− keyMap: DefaultKeyMap(),
111−
109+ app: app,
110+ keyMap: DefaultKeyMap(),
112111 header: header.New(app.LSPClients),
113112 sidebar: sidebar.New(app.History, app.LSPClients, false),
114113 chat: chat.New(app),
@@ -120,19 +119,26 @@ func New(app *app.App) ChatPage {
120119
121120func (p *chatPage) Init() tea.Cmd {
122121 cfg := config.Get()
123− if config.HasInitialDataConfig() {
124− if b, _ := config.ProjectNeedsInitialization(); b {
125− p.state = ChatStateInitProject
126− } else {
127− p.state = ChatStateNewMessage
128− p.focusedPane = PanelTypeEditor
129− }
130− }
131−
132122 compact := cfg.Options.TUI.CompactMode
133123 p.compact = compact
134124 p.forceCompact = compact
135125 p.sidebar.SetCompactMode(p.compact)
126+
127+ // Set splash state based on config
128+ if !config.HasInitialDataConfig() {
129+ // First-time setup: show model selection
130+ p.splash.SetOnboarding(true)
131+ p.splashFullScreen = true
132+ } else if b, _ := config.ProjectNeedsInitialization(); b {
133+ // Project needs CRUSH.md initialization
134+ p.splash.SetProjectInit(true)
135+ p.splashFullScreen = true
136+ } else {
137+ // Ready to chat: focus editor, splash in background
138+ p.focusedPane = PanelTypeEditor
139+ p.splashFullScreen = false
140+ }
141+
136142 return tea.Batch(
137143 p.header.Init(),
138144 p.sidebar.Init(),
@@ -148,7 +154,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
148154 case tea.WindowSizeMsg:
149155 return p, p.SetSize(msg.Width, msg.Height)
150156 case CancelTimerExpiredMsg:
151− p.canceling = false
157+ p.isCanceling = false
152158 return p, nil
153159 case chat.SendMsg:
154160 return p, p.sendMessage(msg.Text, msg.Attachments)
@@ -166,7 +172,6 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
166172 }
167173 return p, tea.Batch(p.SetSize(p.width, p.height), cmd)
168174 case pubsub.Event[session.Session]:
169− // this needs to go to header/sidebar
170175 u, cmd := p.header.Update(msg)
171176 p.header = u.(header.Header)
172177 cmds = append(cmds, cmd)
@@ -196,32 +201,33 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
196201 case pubsub.Event[message.Message],
197202 anim.StepMsg,
198203 spinner.TickMsg:
199− // this needs to go to chat
200204 u, cmd := p.chat.Update(msg)
201205 p.chat = u.(chat.MessageListCmp)
202206 cmds = append(cmds, cmd)
203207 return p, tea.Batch(cmds...)
204208
205209 case pubsub.Event[history.File], sidebar.SessionFilesMsg:
206− // this needs to go to sidebar
207210 u, cmd := p.sidebar.Update(msg)
208211 p.sidebar = u.(sidebar.Sidebar)
209212 cmds = append(cmds, cmd)
210213 return p, tea.Batch(cmds...)
211214
212215 case commands.CommandRunCustomMsg:
213− // Check if the agent is busy before executing custom commands
214216 if p.app.CoderAgent.IsBusy() {
215217 return p, util.ReportWarn("Agent is busy, please wait before executing a command...")
216218 }
217219
218− // Handle custom command execution
219220 cmd := p.sendMessage(msg.Content, nil)
220221 if cmd != nil {
221222 return p, cmd
222223 }
223224 case splash.OnboardingCompleteMsg:
224− p.state = ChatStateNewMessage
225+ p.splashFullScreen = false
226+ if b, _ := config.ProjectNeedsInitialization(); b {
227+ p.splash.SetProjectInit(true)
228+ p.splashFullScreen = true
229+ return p, p.SetSize(p.width, p.height)
230+ }
225231 err := p.app.InitCoderAgent()
226232 if err != nil {
227233 return p, util.ReportError(err)
@@ -241,7 +247,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
241247 return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Name)
242248 }
243249 case key.Matches(msg, p.keyMap.Tab):
244− if p.state == ChatStateOnboarding || p.state == ChatStateInitProject {
250+ if p.session.ID == "" {
245251 u, cmd := p.splash.Update(msg)
246252 p.splash = u.(splash.Splash)
247253 return p, cmd
@@ -255,7 +261,6 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
255261 return p, nil
256262 }
257263
258− // Send the key press to the focused pane
259264 switch p.focusedPane {
260265 case PanelTypeChat:
261266 u, cmd := p.chat.Update(msg)
@@ -277,22 +282,25 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
277282func (p *chatPage) View() tea.View {
278283 var chatView tea.View
279284 t := styles.CurrentTheme()
280− switch p.state {
281− case ChatStateOnboarding, ChatStateInitProject:
282− chatView = p.splash.View()
283− case ChatStateNewMessage:
284− editorView := p.editor.View()
285− chatView = tea.NewView(
286− lipgloss.JoinVertical(
287− lipgloss.Left,
288− t.S().Base.Render(
289− p.splash.View().String(),
285+
286+ if p.session.ID == "" {
287+ splashView := p.splash.View()
288+ // Full screen during onboarding or project initialization
289+ if p.splashFullScreen {
290+ chatView = splashView
291+ } else {
292+ // Show splash + editor for new message state
293+ editorView := p.editor.View()
294+ chatView = tea.NewView(
295+ lipgloss.JoinVertical(
296+ lipgloss.Left,
297+ t.S().Base.Render(splashView.String()),
298+ editorView.String(),
290299 ),
291− editorView.String(),
292− ),
293− )
294− chatView.SetCursor(editorView.Cursor())
295− case ChatStateInSession:
300+ )
301+ chatView.SetCursor(editorView.Cursor())
302+ }
303+ } else {
296304 messagesView := p.chat.View()
297305 editorView := p.editor.View()
298306 if p.compact {
@@ -322,8 +330,6 @@ func (p *chatPage) View() tea.View {
322330 )
323331 chatView.SetCursor(editorView.Cursor())
324332 }
325− default:
326− chatView = tea.NewView("Unknown chat state")
327333 }
328334
329335 layers := []*lipgloss.Layer{
@@ -398,22 +404,22 @@ func (p *chatPage) SetSize(width, height int) tea.Cmd {
398404 p.width = width
399405 p.height = height
400406 var cmds []tea.Cmd
401− switch p.state {
402− case ChatStateOnboarding, ChatStateInitProject:
403− // here we should just have the splash screen
404− cmds = append(cmds, p.splash.SetSize(width, height))
405− case ChatStateNewMessage:
406− cmds = append(cmds, p.splash.SetSize(width, height-EditorHeight))
407− cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
408− cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
409− case ChatStateInSession:
407+
408+ if p.session.ID == "" {
409+ if p.splashFullScreen {
410+ cmds = append(cmds, p.splash.SetSize(width, height))
411+ } else {
412+ cmds = append(cmds, p.splash.SetSize(width, height-EditorHeight))
413+ cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
414+ cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
415+ }
416+ } else {
410417 if p.compact {
411418 cmds = append(cmds, p.chat.SetSize(width, height-EditorHeight-HeaderHeight))
412− // In compact mode, the sidebar is shown in the details section, the width needs to be adjusted for the padding and border
413− p.detailsWidth = width - 2 // because of position
414− cmds = append(cmds, p.sidebar.SetSize(p.detailsWidth-2, p.detailsHeight-2)) // adjust for border
419+ p.detailsWidth = width - DetailsPositioning
420+ cmds = append(cmds, p.sidebar.SetSize(p.detailsWidth-LeftRightBorders, p.detailsHeight-TopBottomBorders))
415421 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
416− cmds = append(cmds, p.header.SetWidth(width-1))
422+ cmds = append(cmds, p.header.SetWidth(width-BorderWidth))
417423 } else {
418424 cmds = append(cmds, p.chat.SetSize(width-SideBarWidth, height-EditorHeight))
419425 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
@@ -425,17 +431,13 @@ func (p *chatPage) SetSize(width, height int) tea.Cmd {
425431}
426432
427433func (p *chatPage) newSession() tea.Cmd {
428− if p.state != ChatStateInSession {
429− // Cannot start a new session if we are not in the session state
434+ if p.session.ID == "" {
430435 return nil
431436 }
432437
433− // blank session
434438 p.session = session.Session{}
435− p.state = ChatStateNewMessage
436439 p.focusedPane = PanelTypeEditor
437− p.canceling = false
438− // Reset the chat and editor components
440+ p.isCanceling = false
439441 return tea.Batch(
440442 util.CmdHandler(chat.SessionClearedMsg{}),
441443 p.SetSize(p.width, p.height),
@@ -449,11 +451,8 @@ func (p *chatPage) setSession(session session.Session) tea.Cmd {
449451
450452 var cmds []tea.Cmd
451453 p.session = session
452− // We want to first resize the components
453− if p.state != ChatStateInSession {
454− p.state = ChatStateInSession
455− cmds = append(cmds, p.SetSize(p.width, p.height))
456− }
454+
455+ cmds = append(cmds, p.SetSize(p.width, p.height))
457456 cmds = append(cmds, p.chat.SetSession(session))
458457 cmds = append(cmds, p.sidebar.SetSession(session))
459458 cmds = append(cmds, p.header.SetSession(session))
@@ -463,8 +462,7 @@ func (p *chatPage) setSession(session session.Session) tea.Cmd {
463462}
464463
465464func (p *chatPage) changeFocus() {
466− if p.state != ChatStateInSession {
467− // Cannot change focus if we are not in the session state
465+ if p.session.ID == "" {
468466 return
469467 }
470468 switch p.focusedPane {
@@ -480,25 +478,22 @@ func (p *chatPage) changeFocus() {
480478}
481479
482480func (p *chatPage) cancel() tea.Cmd {
483− if p.state != ChatStateInSession || !p.app.CoderAgent.IsBusy() {
484− // Cannot cancel if we are not in the session state
481+ if p.session.ID == "" || !p.app.CoderAgent.IsBusy() {
485482 return nil
486483 }
487484
488− // second press of cancel key will actually cancel the session
489− if p.canceling {
490− p.canceling = false
485+ if p.isCanceling {
486+ p.isCanceling = false
491487 p.app.CoderAgent.Cancel(p.session.ID)
492488 return nil
493489 }
494490
495− p.canceling = true
491+ p.isCanceling = true
496492 return cancelTimerCmd()
497493}
498494
499495func (p *chatPage) showDetails() {
500− if p.state != ChatStateInSession || !p.compact {
501− // Cannot show details if we are not in the session state or if we are not in compact mode
496+ if p.session.ID == "" || !p.compact {
502497 return
503498 }
504499 p.showingDetails = !p.showingDetails
@@ -508,8 +503,7 @@ func (p *chatPage) showDetails() {
508503func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
509504 session := p.session
510505 var cmds []tea.Cmd
511− if p.state != ChatStateInSession {
512− // branch new session
506+ if p.session.ID == "" {
513507 newSession, err := p.app.Sessions.Create(context.Background(), "New Session")
514508 if err != nil {
515509 return util.ReportError(err)
@@ -531,7 +525,7 @@ func (p *chatPage) Bindings() []key.Binding {
531525 }
532526 if p.app.CoderAgent != nil && p.app.CoderAgent.IsBusy() {
533527 cancelBinding := p.keyMap.Cancel
534− if p.canceling {
528+ if p.isCanceling {
535529 cancelBinding = key.NewBinding(
536530 key.WithKeys("esc"),
537531 key.WithHelp("esc", "press again to cancel"),