fix: tui: use KeyPressMsg instead of KeyMsg

Ayman Bagabas created

The `tea.KeyMsg` type can match both key presses and releases, which can
lead to unexpected behavior in the TUI. Use `tea.KeyPressMsg` explicitly
to ensure we match only key presses.

Change summary

internal/format/spinner.go                   |   2 
internal/tui/components/chat/list.go         | 451 ----------------------
internal/tui/components/core/list/list.go    |   2 
internal/tui/components/dialog/arguments.go  |   2 
internal/tui/components/dialog/commands.go   |   2 
internal/tui/components/dialog/complete.go   |   2 
internal/tui/components/dialog/filepicker.go |   2 
internal/tui/components/dialog/init.go       |   4 
internal/tui/components/dialog/models.go     |   2 
internal/tui/components/dialog/permission.go |   2 
internal/tui/components/dialog/quit.go       |   2 
internal/tui/components/dialog/session.go    |   2 
internal/tui/components/dialog/theme.go      |   2 
internal/tui/components/util/simple-list.go  |   2 
internal/tui/page/chat.go                    |   4 
internal/tui/tui.go                          |  18 
16 files changed, 25 insertions(+), 476 deletions(-)

Detailed changes

internal/format/spinner.go 🔗

@@ -31,7 +31,7 @@ func (m spinnerModel) Init() tea.Cmd {
 
 func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		m.quitting = true
 		return m, tea.Quit
 	case spinner.TickMsg:

internal/tui/components/chat/list.go 🔗

@@ -2,48 +2,6 @@ package chat
 
 import "github.com/charmbracelet/bubbles/v2/key"
 
-// import (
-//
-//	"context"
-//	"fmt"
-//	"math"
-//
-//	"github.com/charmbracelet/bubbles/v2/key"
-//	"github.com/charmbracelet/bubbles/v2/spinner"
-//	"github.com/charmbracelet/bubbles/v2/viewport"
-//	tea "github.com/charmbracelet/bubbletea/v2"
-//	"github.com/charmbracelet/lipgloss/v2"
-//	"github.com/opencode-ai/opencode/internal/app"
-//	"github.com/opencode-ai/opencode/internal/message"
-//	"github.com/opencode-ai/opencode/internal/pubsub"
-//	"github.com/opencode-ai/opencode/internal/session"
-//	"github.com/opencode-ai/opencode/internal/tui/components/dialog"
-//	"github.com/opencode-ai/opencode/internal/tui/styles"
-//	"github.com/opencode-ai/opencode/internal/tui/theme"
-//	"github.com/opencode-ai/opencode/internal/tui/util"
-//
-// )
-//
-//	type cacheItem struct {
-//		width   int
-//		content []uiMessage
-//	}
-//
-//	type messagesCmp struct {
-//		app           *app.App
-//		width, height int
-//		viewport      viewport.Model
-//		session       session.Session
-//		messages      []message.Message
-//		uiMessages    []uiMessage
-//		currentMsgID  string
-//		cachedContent map[string]cacheItem
-//		spinner       spinner.Model
-//		rendering     bool
-//		attachments   viewport.Model
-//	}
-//
-// type renderFinishedMsg struct{}
 type MessageKeys struct {
 	PageDown     key.Binding
 	PageUp       key.Binding
@@ -69,412 +27,3 @@ var messageKeys = MessageKeys{
 		key.WithHelp("ctrl+d", "½ page down"),
 	),
 }
-
-//
-// func (m *messagesCmp) Init() tea.Cmd {
-// 	return tea.Batch(m.viewport.Init(), m.spinner.Tick)
-// }
-//
-// func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
-// 	var cmds []tea.Cmd
-// 	switch msg := msg.(type) {
-// 	case dialog.ThemeChangedMsg:
-// 		m.rerender()
-// 		return m, nil
-// 	case SessionSelectedMsg:
-// 		if msg.ID != m.session.ID {
-// 			cmd := m.SetSession(msg)
-// 			return m, cmd
-// 		}
-// 		return m, nil
-// 	case SessionClearedMsg:
-// 		m.session = session.Session{}
-// 		m.messages = make([]message.Message, 0)
-// 		m.currentMsgID = ""
-// 		m.rendering = false
-// 		return m, nil
-//
-// 	case tea.KeyMsg:
-// 		if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
-// 			key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
-// 			u, cmd := m.viewport.Update(msg)
-// 			m.viewport = u
-// 			cmds = append(cmds, cmd)
-// 		}
-//
-// 	case renderFinishedMsg:
-// 		m.rendering = false
-// 		m.viewport.GotoBottom()
-// 	case pubsub.Event[session.Session]:
-// 		if msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.session.ID {
-// 			m.session = msg.Payload
-// 			if m.session.SummaryMessageID == m.currentMsgID {
-// 				delete(m.cachedContent, m.currentMsgID)
-// 				m.renderView()
-// 			}
-// 		}
-// 	case pubsub.Event[message.Message]:
-// 		needsRerender := false
-// 		if msg.Type == pubsub.CreatedEvent {
-// 			if msg.Payload.SessionID == m.session.ID {
-//
-// 				messageExists := false
-// 				for _, v := range m.messages {
-// 					if v.ID == msg.Payload.ID {
-// 						messageExists = true
-// 						break
-// 					}
-// 				}
-//
-// 				if !messageExists {
-// 					if len(m.messages) > 0 {
-// 						lastMsgID := m.messages[len(m.messages)-1].ID
-// 						delete(m.cachedContent, lastMsgID)
-// 					}
-//
-// 					m.messages = append(m.messages, msg.Payload)
-// 					delete(m.cachedContent, m.currentMsgID)
-// 					m.currentMsgID = msg.Payload.ID
-// 					needsRerender = true
-// 				}
-// 			}
-// 			// There are tool calls from the child task
-// 			for _, v := range m.messages {
-// 				for _, c := range v.ToolCalls() {
-// 					if c.ID == msg.Payload.SessionID {
-// 						delete(m.cachedContent, v.ID)
-// 						needsRerender = true
-// 					}
-// 				}
-// 			}
-// 		} else if msg.Type == pubsub.UpdatedEvent && msg.Payload.SessionID == m.session.ID {
-// 			for i, v := range m.messages {
-// 				if v.ID == msg.Payload.ID {
-// 					m.messages[i] = msg.Payload
-// 					delete(m.cachedContent, msg.Payload.ID)
-// 					needsRerender = true
-// 					break
-// 				}
-// 			}
-// 		}
-// 		if needsRerender {
-// 			m.renderView()
-// 			if len(m.messages) > 0 {
-// 				if (msg.Type == pubsub.CreatedEvent) ||
-// 					(msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.messages[len(m.messages)-1].ID) {
-// 					m.viewport.GotoBottom()
-// 				}
-// 			}
-// 		}
-// 	}
-//
-// 	spinner, cmd := m.spinner.Update(msg)
-// 	m.spinner = spinner
-// 	cmds = append(cmds, cmd)
-// 	return m, tea.Batch(cmds...)
-// }
-//
-// func (m *messagesCmp) IsAgentWorking() bool {
-// 	return m.app.CoderAgent.IsSessionBusy(m.session.ID)
-// }
-//
-// func formatTimeDifference(unixTime1, unixTime2 int64) string {
-// 	diffSeconds := float64(math.Abs(float64(unixTime2 - unixTime1)))
-//
-// 	if diffSeconds < 60 {
-// 		return fmt.Sprintf("%.1fs", diffSeconds)
-// 	}
-//
-// 	minutes := int(diffSeconds / 60)
-// 	seconds := int(diffSeconds) % 60
-// 	return fmt.Sprintf("%dm%ds", minutes, seconds)
-// }
-//
-// func (m *messagesCmp) renderView() {
-// 	m.uiMessages = make([]uiMessage, 0)
-// 	pos := 0
-// 	baseStyle := styles.BaseStyle()
-//
-// 	if m.width == 0 {
-// 		return
-// 	}
-// 	for inx, msg := range m.messages {
-// 		switch msg.Role {
-// 		case message.User:
-// 			if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
-// 				m.uiMessages = append(m.uiMessages, cache.content...)
-// 				continue
-// 			}
-// 			userMsg := renderUserMessage(
-// 				msg,
-// 				msg.ID == m.currentMsgID,
-// 				m.width,
-// 				pos,
-// 			)
-// 			m.uiMessages = append(m.uiMessages, userMsg)
-// 			m.cachedContent[msg.ID] = cacheItem{
-// 				width:   m.width,
-// 				content: []uiMessage{userMsg},
-// 			}
-// 			pos += userMsg.height + 1 // + 1 for spacing
-// 		case message.Assistant:
-// 			if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
-// 				m.uiMessages = append(m.uiMessages, cache.content...)
-// 				continue
-// 			}
-// 			isSummary := m.session.SummaryMessageID == msg.ID
-//
-// 			assistantMessages := renderAssistantMessage(
-// 				msg,
-// 				inx,
-// 				m.messages,
-// 				m.app.Messages,
-// 				m.currentMsgID,
-// 				isSummary,
-// 				m.width,
-// 				pos,
-// 			)
-// 			for _, msg := range assistantMessages {
-// 				m.uiMessages = append(m.uiMessages, msg)
-// 				pos += msg.height + 1 // + 1 for spacing
-// 			}
-// 			m.cachedContent[msg.ID] = cacheItem{
-// 				width:   m.width,
-// 				content: assistantMessages,
-// 			}
-// 		}
-// 	}
-//
-// 	messages := make([]string, 0)
-// 	for _, v := range m.uiMessages {
-// 		messages = append(messages, lipgloss.JoinVertical(lipgloss.Left, v.content),
-// 			baseStyle.
-// 				Width(m.width).
-// 				Render(
-// 					"",
-// 				),
-// 		)
-// 	}
-//
-// 	m.viewport.SetContent(
-// 		baseStyle.
-// 			Width(m.width).
-// 			Render(
-// 				lipgloss.JoinVertical(
-// 					lipgloss.Top,
-// 					messages...,
-// 				),
-// 			),
-// 	)
-// }
-//
-// func (m *messagesCmp) View() string {
-// 	baseStyle := styles.BaseStyle()
-//
-// 	if m.rendering {
-// 		return baseStyle.
-// 			Width(m.width).
-// 			Render(
-// 				lipgloss.JoinVertical(
-// 					lipgloss.Top,
-// 					"Loading...",
-// 					m.working(),
-// 					m.help(),
-// 				),
-// 			)
-// 	}
-// 	if len(m.messages) == 0 {
-// 		content := baseStyle.
-// 			Width(m.width).
-// 			Height(m.height - 1).
-// 			Render(
-// 				initialScreen(),
-// 			)
-//
-// 		return baseStyle.
-// 			Width(m.width).
-// 			Render(
-// 				lipgloss.JoinVertical(
-// 					lipgloss.Top,
-// 					content,
-// 					"",
-// 					m.help(),
-// 				),
-// 			)
-// 	}
-//
-// 	return baseStyle.
-// 		Width(m.width).
-// 		Render(
-// 			lipgloss.JoinVertical(
-// 				lipgloss.Top,
-// 				m.viewport.View(),
-// 				m.working(),
-// 				m.help(),
-// 			),
-// 		)
-// }
-//
-// func hasToolsWithoutResponse(messages []message.Message) bool {
-// 	toolCalls := make([]message.ToolCall, 0)
-// 	toolResults := make([]message.ToolResult, 0)
-// 	for _, m := range messages {
-// 		toolCalls = append(toolCalls, m.ToolCalls()...)
-// 		toolResults = append(toolResults, m.ToolResults()...)
-// 	}
-//
-// 	for _, v := range toolCalls {
-// 		found := false
-// 		for _, r := range toolResults {
-// 			if v.ID == r.ToolCallID {
-// 				found = true
-// 				break
-// 			}
-// 		}
-// 		if !found && v.Finished {
-// 			return true
-// 		}
-// 	}
-// 	return false
-// }
-//
-// func hasUnfinishedToolCalls(messages []message.Message) bool {
-// 	toolCalls := make([]message.ToolCall, 0)
-// 	for _, m := range messages {
-// 		toolCalls = append(toolCalls, m.ToolCalls()...)
-// 	}
-// 	for _, v := range toolCalls {
-// 		if !v.Finished {
-// 			return true
-// 		}
-// 	}
-// 	return false
-// }
-//
-// func (m *messagesCmp) working() string {
-// 	text := ""
-// 	if m.IsAgentWorking() && len(m.messages) > 0 {
-// 		t := theme.CurrentTheme()
-// 		baseStyle := styles.BaseStyle()
-//
-// 		task := "Thinking..."
-// 		lastMessage := m.messages[len(m.messages)-1]
-// 		if hasToolsWithoutResponse(m.messages) {
-// 			task = "Waiting for tool response..."
-// 		} else if hasUnfinishedToolCalls(m.messages) {
-// 			task = "Building tool call..."
-// 		} else if !lastMessage.IsFinished() {
-// 			task = "Generating..."
-// 		}
-// 		if task != "" {
-// 			text += baseStyle.
-// 				Width(m.width).
-// 				Foreground(t.Primary()).
-// 				Bold(true).
-// 				Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
-// 		}
-// 	}
-// 	return text
-// }
-//
-// func (m *messagesCmp) help() string {
-// 	t := theme.CurrentTheme()
-// 	baseStyle := styles.BaseStyle()
-//
-// 	text := ""
-//
-// 	if m.app.CoderAgent.IsBusy() {
-// 		text += lipgloss.JoinHorizontal(
-// 			lipgloss.Left,
-// 			baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
-// 			baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
-// 			baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to exit cancel"),
-// 		)
-// 	} else {
-// 		text += lipgloss.JoinHorizontal(
-// 			lipgloss.Left,
-// 			baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
-// 			baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
-// 			baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send the message,"),
-// 			baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" write"),
-// 			baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
-// 			baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" and enter to add a new line"),
-// 		)
-// 	}
-// 	return baseStyle.
-// 		Width(m.width).
-// 		Render(text)
-// }
-//
-// func (m *messagesCmp) rerender() {
-// 	for _, msg := range m.messages {
-// 		delete(m.cachedContent, msg.ID)
-// 	}
-// 	m.renderView()
-// }
-//
-// func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
-// 	if m.width == width && m.height == height {
-// 		return nil
-// 	}
-// 	m.width = width
-// 	m.height = height
-// 	m.viewport.SetWidth(width)
-// 	m.viewport.SetHeight(height - 2)
-// 	m.attachments.SetWidth(width + 40)
-// 	m.attachments.SetHeight(3)
-// 	m.rerender()
-// 	return nil
-// }
-//
-// func (m *messagesCmp) GetSize() (int, int) {
-// 	return m.width, m.height
-// }
-//
-// func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
-// 	if m.session.ID == session.ID {
-// 		return nil
-// 	}
-// 	m.session = session
-// 	messages, err := m.app.Messages.List(context.Background(), session.ID)
-// 	if err != nil {
-// 		return util.ReportError(err)
-// 	}
-// 	m.messages = messages
-// 	if len(m.messages) > 0 {
-// 		m.currentMsgID = m.messages[len(m.messages)-1].ID
-// 	}
-// 	delete(m.cachedContent, m.currentMsgID)
-// 	m.rendering = true
-// 	return func() tea.Msg {
-// 		m.renderView()
-// 		return renderFinishedMsg{}
-// 	}
-// }
-//
-// func (m *messagesCmp) BindingKeys() []key.Binding {
-// 	return []key.Binding{
-// 		m.viewport.KeyMap.PageDown,
-// 		m.viewport.KeyMap.PageUp,
-// 		m.viewport.KeyMap.HalfPageUp,
-// 		m.viewport.KeyMap.HalfPageDown,
-// 	}
-// }
-//
-// func NewMessagesCmp(app *app.App) util.Model {
-// 	s := spinner.New()
-// 	s.Spinner = spinner.Pulse
-// 	vp := viewport.New()
-// 	attachmets := viewport.New()
-// 	vp.KeyMap.PageUp = messageKeys.PageUp
-// 	vp.KeyMap.PageDown = messageKeys.PageDown
-// 	vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
-// 	vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
-// 	return &messagesCmp{
-// 		app:           app,
-// 		cachedContent: make(map[string]cacheItem),
-// 		viewport:      vp,
-// 		spinner:       s,
-// 		attachments:   attachmets,
-// 	}
-// }

internal/tui/components/core/list/list.go 🔗

@@ -117,7 +117,7 @@ func (m *model) Init() tea.Cmd {
 func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	var cmds []tea.Cmd
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, m.keymap.Down) || key.Matches(msg, m.keymap.NDown):
 			if m.reverse {

internal/tui/components/dialog/arguments.go 🔗

@@ -118,7 +118,7 @@ func (m MultiArgumentsDialogCmp) Init() tea.Cmd {
 func (m MultiArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	var cmds []tea.Cmd
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
 			return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{

internal/tui/components/dialog/commands.go 🔗

@@ -90,7 +90,7 @@ func (c *commandDialogCmp) Init() tea.Cmd {
 func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	var cmds []tea.Cmd
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, commandKeys.Enter):
 			selectedItem, idx := c.listView.GetSelectedItem()

internal/tui/components/dialog/complete.go 🔗

@@ -136,7 +136,7 @@ func (c *completionDialogCmp) close() tea.Cmd {
 func (c *completionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	var cmds []tea.Cmd
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		if c.pseudoSearchTextArea.Focused() {
 
 			if !key.Matches(msg, completionDialogKeys.Complete) {

internal/tui/components/dialog/filepicker.go 🔗

@@ -126,7 +126,7 @@ func (f *filepickerCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		f.viewport.SetHeight(22)
 		f.cursor = 0
 		f.getCurrentFileBelowCursor()
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		if f.cwd.Focused() {
 			f.cwd, cmd = f.cwd.Update(msg)
 		}

internal/tui/components/dialog/init.go 🔗

@@ -70,7 +70,7 @@ func (m InitDialogCmp) Init() tea.Cmd {
 // Update implements tea.Model.
 func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
 			return m, util.CmdHandler(CloseInitDialogMsg{Initialize: false})
@@ -95,7 +95,7 @@ func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 func (m InitDialogCmp) View() string {
 	t := theme.CurrentTheme()
 	baseStyle := styles.BaseStyle()
-	
+
 	// Calculate width needed for content
 	maxWidth := 60 // Width for explanation text
 

internal/tui/components/dialog/models.go 🔗

@@ -111,7 +111,7 @@ func (m *modelDialogCmp) Init() tea.Cmd {
 
 func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, modelKeys.Up) || key.Matches(msg, modelKeys.K):
 			m.moveSelectionUp()

internal/tui/components/dialog/permission.go 🔗

@@ -107,7 +107,7 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		cmds = append(cmds, cmd)
 		p.markdownCache = make(map[string]string)
 		p.diffCache = make(map[string]string)
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, permissionsKeys.Right) || key.Matches(msg, permissionsKeys.Tab):
 			p.selectedOption = (p.selectedOption + 1) % 3

internal/tui/components/dialog/quit.go 🔗

@@ -62,7 +62,7 @@ func (q *quitDialogCmp) Init() tea.Cmd {
 
 func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, helpKeys.LeftRight) || key.Matches(msg, helpKeys.Tab):
 			q.selectedNo = !q.selectedNo

internal/tui/components/dialog/session.go 🔗

@@ -77,7 +77,7 @@ func (s *sessionDialogCmp) Init() tea.Cmd {
 
 func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, sessionKeys.Up) || key.Matches(msg, sessionKeys.K):
 			if s.selectedIdx > 0 {

internal/tui/components/dialog/theme.go 🔗

@@ -86,7 +86,7 @@ func (t *themeDialogCmp) Init() tea.Cmd {
 
 func (t *themeDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, themeKeys.Up) || key.Matches(msg, themeKeys.K):
 			if t.selectedIdx > 0 {

internal/tui/components/util/simple-list.go 🔗

@@ -66,7 +66,7 @@ func (c *simpleListCmp[T]) Init() tea.Cmd {
 
 func (c *simpleListCmp[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, simpleListKeys.Up) || (c.useAlphaNumericKeys && key.Matches(msg, simpleListKeys.UpAlpha)):
 			if c.selectedIdx > 0 {

internal/tui/page/chat.go 🔗

@@ -102,7 +102,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			}
 		}
 		p.session = msg
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, keyMap.ShowCompletionDialog):
 			p.showCompletionDialog = true
@@ -128,7 +128,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		cmds = append(cmds, contextCmd)
 
 		// Doesn't forward event if enter key is pressed
-		if keyMsg, ok := msg.(tea.KeyMsg); ok {
+		if keyMsg, ok := msg.(tea.KeyPressMsg); ok {
 			if keyMsg.String() == "enter" {
 				return p, tea.Batch(cmds...)
 			}

internal/tui/tui.go 🔗

@@ -448,7 +448,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		}
 		return a, nil
 
-	case tea.KeyMsg:
+	case tea.KeyPressMsg:
 		// If multi-arguments dialog is open, let it handle the key press first
 		if a.showMultiArgumentsDialog {
 			args, cmd := a.multiArgumentsDialog.Update(msg)
@@ -588,7 +588,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		a.filepicker = f.(dialog.FilepickerCmp)
 		cmds = append(cmds, filepickerCmd)
 		// Only block key messages send all other messages down
-		if _, ok := msg.(tea.KeyMsg); ok {
+		if _, ok := msg.(tea.KeyPressMsg); ok {
 			return a, tea.Batch(cmds...)
 		}
 	}
@@ -598,7 +598,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		a.quit = q.(dialog.QuitDialog)
 		cmds = append(cmds, quitCmd)
 		// Only block key messages send all other messages down
-		if _, ok := msg.(tea.KeyMsg); ok {
+		if _, ok := msg.(tea.KeyPressMsg); ok {
 			return a, tea.Batch(cmds...)
 		}
 	}
@@ -607,7 +607,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		a.permissions = d.(dialog.PermissionDialogCmp)
 		cmds = append(cmds, permissionsCmd)
 		// Only block key messages send all other messages down
-		if _, ok := msg.(tea.KeyMsg); ok {
+		if _, ok := msg.(tea.KeyPressMsg); ok {
 			return a, tea.Batch(cmds...)
 		}
 	}
@@ -617,7 +617,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		a.sessionDialog = d.(dialog.SessionDialog)
 		cmds = append(cmds, sessionCmd)
 		// Only block key messages send all other messages down
-		if _, ok := msg.(tea.KeyMsg); ok {
+		if _, ok := msg.(tea.KeyPressMsg); ok {
 			return a, tea.Batch(cmds...)
 		}
 	}
@@ -627,7 +627,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		a.commandDialog = d.(dialog.CommandDialog)
 		cmds = append(cmds, commandCmd)
 		// Only block key messages send all other messages down
-		if _, ok := msg.(tea.KeyMsg); ok {
+		if _, ok := msg.(tea.KeyPressMsg); ok {
 			return a, tea.Batch(cmds...)
 		}
 	}
@@ -637,7 +637,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		a.modelDialog = d.(dialog.ModelDialog)
 		cmds = append(cmds, modelCmd)
 		// Only block key messages send all other messages down
-		if _, ok := msg.(tea.KeyMsg); ok {
+		if _, ok := msg.(tea.KeyPressMsg); ok {
 			return a, tea.Batch(cmds...)
 		}
 	}
@@ -647,7 +647,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		a.initDialog = d.(dialog.InitDialogCmp)
 		cmds = append(cmds, initCmd)
 		// Only block key messages send all other messages down
-		if _, ok := msg.(tea.KeyMsg); ok {
+		if _, ok := msg.(tea.KeyPressMsg); ok {
 			return a, tea.Batch(cmds...)
 		}
 	}
@@ -657,7 +657,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		a.themeDialog = d.(dialog.ThemeDialog)
 		cmds = append(cmds, themeCmd)
 		// Only block key messages send all other messages down
-		if _, ok := msg.(tea.KeyMsg); ok {
+		if _, ok := msg.(tea.KeyPressMsg); ok {
 			return a, tea.Batch(cmds...)
 		}
 	}