Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions pkg/tui/components/messages/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type Model interface {
LoadFromSession(sess *session.Session) tea.Cmd

ScrollToBottom() tea.Cmd
AdjustBottomSlack(delta int)
}

// renderedItem represents a cached rendered message with position information
Expand Down Expand Up @@ -1229,6 +1230,13 @@ func (m *model) ScrollToBottom() tea.Cmd {
}
}

func (m *model) AdjustBottomSlack(delta int) {
if delta == 0 {
return
}
m.bottomSlack = max(0, m.bottomSlack+delta)
}

// contentWidth returns the width available for content.
// Always reserves 2 chars for scrollbar (space + bar) to prevent layout shifts.
func (m *model) contentWidth() int {
Expand Down
54 changes: 43 additions & 11 deletions pkg/tui/page/chat/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,14 +421,47 @@ func (p *chatPage) setWorking(working bool) tea.Cmd {

// setPendingResponse sets the pending response state.
// When true, a spinner is shown below the messages while waiting for the first chunk.
// Resizes messages to leave room for the spinner; bottomSlack smooths the transition.
func (p *chatPage) setPendingResponse(pending bool) tea.Cmd {
p.pendingResponse = pending
if pending {
// Reset spinner to get a fresh random phrase
p.pendingResponse = pending

// Resize messages to account for spinner space (3 lines: 2 newlines + 1 spinner)
mainWidth, messagesHeight := p.messagesSize()
resizeCmd := p.messages.SetSize(mainWidth, messagesHeight)

p.pendingSpinner = p.pendingSpinner.Reset()
return p.pendingSpinner.Init()
return tea.Batch(resizeCmd, p.pendingSpinner.Init())
}
return nil

p.pendingResponse = pending

// Resize messages to reclaim spinner space and add slack to prevent a visual jump.
mainWidth, messagesHeight := p.messagesSize()
resizeCmd := p.messages.SetSize(mainWidth, messagesHeight)
p.messages.AdjustBottomSlack(pendingSpinnerHeight)

return resizeCmd
}

// pendingSpinnerHeight is the space taken by the pending spinner (2 newlines + 1 line)
const pendingSpinnerHeight = 3

func (p *chatPage) messagesSize() (int, int) {
innerWidth := p.width - 2
var mainWidth int
if p.width >= minWindowWidth {
mainWidth = max(1, innerWidth-sidebarWidth)
} else {
mainWidth = max(innerWidth, 1)
}

messagesHeight := p.chatHeight
if p.pendingResponse {
messagesHeight -= pendingSpinnerHeight
}

return mainWidth, max(1, messagesHeight)
}

// View renders the chat page
Expand All @@ -453,13 +486,12 @@ func (p *chatPage) View() string {
}
}

if p.width >= minWindowWidth {
// Ensure we don't exceed available space
chatWidth := max(1, innerWidth-sidebarWidth)
mainWidth, _ := p.messagesSize()

if p.width >= minWindowWidth {
chatView := styles.ChatStyle.
Height(p.chatHeight).
Width(chatWidth).
Width(mainWidth).
Render(messagesView)

sidebarView := lipgloss.NewStyle().
Expand All @@ -478,7 +510,7 @@ func (p *chatPage) View() string {

chatView := styles.ChatStyle.
Height(p.chatHeight).
Width(innerWidth).
Width(mainWidth).
Render(messagesView)

sidebarView := lipgloss.NewStyle().
Expand Down Expand Up @@ -551,7 +583,6 @@ func (p *chatPage) SetSize(width, height int) tea.Cmd {
)
} else {
const horizontalSidebarHeight = 3
mainWidth = max(innerWidth, 1)
p.chatHeight = max(1, height-actualEditorHeight-horizontalSidebarHeight-2) // -1 for resize handle, -1 for empty line before status bar
p.sidebar.SetMode(sidebar.ModeHorizontal)
cmds = append(cmds,
Expand All @@ -562,8 +593,9 @@ func (p *chatPage) SetSize(width, height int) tea.Cmd {
}

// Set component sizes
mainWidth, messagesHeight := p.messagesSize()
cmds = append(cmds,
p.messages.SetSize(mainWidth, p.chatHeight),
p.messages.SetSize(mainWidth, messagesHeight),
)

return tea.Batch(cmds...)
Expand Down