remove edit/normal mode

Kujtim Hoxha created

Change summary

README.md                                    |  2 
internal/tui/components/chat/editor.go       | 64 +++++++--------------
internal/tui/components/chat/list.go         | 63 ++++++++++++++-------
internal/tui/components/dialog/commands.go   |  2 
internal/tui/components/dialog/help.go       |  2 
internal/tui/components/dialog/init.go       | 12 +---
internal/tui/components/dialog/permission.go | 14 +---
internal/tui/components/dialog/session.go    |  4 
internal/tui/page/chat.go                    | 30 +++------
internal/tui/tui.go                          | 15 +---
10 files changed, 92 insertions(+), 116 deletions(-)

Detailed changes

README.md 🔗

@@ -1,4 +1,4 @@
-# OpenCode
+# ⌬ OpenCode
 
 > **⚠️ Early Development Notice:** This project is in early development and is not yet ready for production use. Features may change, break, or be incomplete. Use at your own risk.
 

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

@@ -26,7 +26,6 @@ type FocusEditorMsg bool
 type focusedEditorKeyMaps struct {
 	Send       key.Binding
 	OpenEditor key.Binding
-	Blur       key.Binding
 }
 
 type bluredEditorKeyMaps struct {
@@ -35,30 +34,11 @@ type bluredEditorKeyMaps struct {
 	OpenEditor key.Binding
 }
 
-var focusedKeyMaps = focusedEditorKeyMaps{
+var KeyMaps = focusedEditorKeyMaps{
 	Send: key.NewBinding(
 		key.WithKeys("ctrl+s"),
 		key.WithHelp("ctrl+s", "send message"),
 	),
-	Blur: key.NewBinding(
-		key.WithKeys("esc"),
-		key.WithHelp("esc", "focus messages"),
-	),
-	OpenEditor: key.NewBinding(
-		key.WithKeys("ctrl+e"),
-		key.WithHelp("ctrl+e", "open editor"),
-	),
-}
-
-var bluredKeyMaps = bluredEditorKeyMaps{
-	Send: key.NewBinding(
-		key.WithKeys("ctrl+s", "enter"),
-		key.WithHelp("ctrl+s/enter", "send message"),
-	),
-	Focus: key.NewBinding(
-		key.WithKeys("i"),
-		key.WithHelp("i", "focus editor"),
-	),
 	OpenEditor: key.NewBinding(
 		key.WithKeys("ctrl+e"),
 		key.WithHelp("ctrl+e", "open editor"),
@@ -88,6 +68,9 @@ func openEditor() tea.Cmd {
 		if err != nil {
 			return util.ReportError(err)
 		}
+		if len(content) == 0 {
+			return util.ReportWarn("Message is empty")
+		}
 		os.Remove(tmpfile.Name())
 		return SendMsg{
 			Text: string(content),
@@ -106,7 +89,6 @@ func (m *editorCmp) send() tea.Cmd {
 
 	value := m.textarea.Value()
 	m.textarea.Reset()
-	m.textarea.Blur()
 	if value == "" {
 		return nil
 	}
@@ -131,26 +113,32 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
 		}
 	case tea.KeyMsg:
-		if key.Matches(msg, focusedKeyMaps.OpenEditor) {
+		if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
+			key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
+			return m, nil
+		}
+		if key.Matches(msg, KeyMaps.OpenEditor) {
 			if m.app.CoderAgent.IsSessionBusy(m.session.ID) {
 				return m, util.ReportWarn("Agent is working, please wait...")
 			}
 			return m, openEditor()
 		}
 		// if the key does not match any binding, return
-		if m.textarea.Focused() && key.Matches(msg, focusedKeyMaps.Send) {
+		if m.textarea.Focused() && key.Matches(msg, KeyMaps.Send) {
 			return m, m.send()
 		}
-		if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Send) {
-			return m, m.send()
-		}
-		if m.textarea.Focused() && key.Matches(msg, focusedKeyMaps.Blur) {
-			m.textarea.Blur()
-			return m, util.CmdHandler(EditorFocusMsg(false))
-		}
-		if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Focus) {
-			m.textarea.Focus()
-			return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
+		
+		// Handle Enter key
+		if m.textarea.Focused() && msg.String() == "enter" {
+			value := m.textarea.Value()
+			if len(value) > 0 && value[len(value)-1] == '\\' {
+				// If the last character is a backslash, remove it and add a newline
+				m.textarea.SetValue(value[:len(value)-1] + "\n")
+				return m, nil
+			} else {
+				// Otherwise, send the message
+				return m, m.send()
+			}
 		}
 	}
 	m.textarea, cmd = m.textarea.Update(msg)
@@ -175,13 +163,7 @@ func (m *editorCmp) GetSize() (int, int) {
 
 func (m *editorCmp) BindingKeys() []key.Binding {
 	bindings := []key.Binding{}
-	if m.textarea.Focused() {
-		bindings = append(bindings, layout.KeyMapToSlice(focusedKeyMaps)...)
-	} else {
-		bindings = append(bindings, layout.KeyMapToSlice(bluredKeyMaps)...)
-	}
-
-	bindings = append(bindings, layout.KeyMapToSlice(m.textarea.KeyMap)...)
+	bindings = append(bindings, layout.KeyMapToSlice(KeyMaps)...)
 	return bindings
 }
 

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

@@ -14,7 +14,6 @@ import (
 	"github.com/kujtimiihoxha/opencode/internal/message"
 	"github.com/kujtimiihoxha/opencode/internal/pubsub"
 	"github.com/kujtimiihoxha/opencode/internal/session"
-	"github.com/kujtimiihoxha/opencode/internal/tui/layout"
 	"github.com/kujtimiihoxha/opencode/internal/tui/styles"
 	"github.com/kujtimiihoxha/opencode/internal/tui/util"
 )
@@ -26,7 +25,6 @@ type cacheItem struct {
 type messagesCmp struct {
 	app           *app.App
 	width, height int
-	writingMode   bool
 	viewport      viewport.Model
 	session       session.Session
 	messages      []message.Message
@@ -38,6 +36,32 @@ type messagesCmp struct {
 }
 type renderFinishedMsg struct{}
 
+type MessageKeys struct {
+	PageDown     key.Binding
+	PageUp       key.Binding
+	HalfPageUp   key.Binding
+	HalfPageDown key.Binding
+}
+
+var messageKeys = MessageKeys{
+	PageDown: key.NewBinding(
+		key.WithKeys("pgdown"),
+		key.WithHelp("f/pgdn", "page down"),
+	),
+	PageUp: key.NewBinding(
+		key.WithKeys("pgup"),
+		key.WithHelp("b/pgup", "page up"),
+	),
+	HalfPageUp: key.NewBinding(
+		key.WithKeys("ctrl+u"),
+		key.WithHelp("ctrl+u", "½ page up"),
+	),
+	HalfPageDown: key.NewBinding(
+		key.WithKeys("ctrl+d", "ctrl+d"),
+		key.WithHelp("ctrl+d", "½ page down"),
+	),
+}
+
 func (m *messagesCmp) Init() tea.Cmd {
 	return tea.Batch(m.viewport.Init(), m.spinner.Tick)
 }
@@ -45,8 +69,7 @@ func (m *messagesCmp) Init() tea.Cmd {
 func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	var cmds []tea.Cmd
 	switch msg := msg.(type) {
-	case EditorFocusMsg:
-		m.writingMode = bool(msg)
+
 	case SessionSelectedMsg:
 		if msg.ID != m.session.ID {
 			cmd := m.SetSession(msg)
@@ -63,10 +86,6 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case renderFinishedMsg:
 		m.rendering = false
 		m.viewport.GotoBottom()
-	case tea.KeyMsg:
-		if m.writingMode {
-			return m, nil
-		}
 	case pubsub.Event[message.Message]:
 		needsRerender := false
 		if msg.Type == pubsub.CreatedEvent {
@@ -326,22 +345,14 @@ func (m *messagesCmp) working() string {
 func (m *messagesCmp) help() string {
 	text := ""
 
-	if m.writingMode {
+	if m.app.CoderAgent.IsBusy() {
 		text += lipgloss.JoinHorizontal(
 			lipgloss.Left,
 			styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
 			styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"),
-			styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit writing mode"),
-		)
-	} else {
-		text += lipgloss.JoinHorizontal(
-			lipgloss.Left,
-			styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
-			styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("i"),
-			styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to start writing"),
+			styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit cancel"),
 		)
 	}
-
 	return styles.BaseStyle.
 		Width(m.width).
 		Render(text)
@@ -398,18 +409,26 @@ func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
 }
 
 func (m *messagesCmp) BindingKeys() []key.Binding {
-	bindings := layout.KeyMapToSlice(m.viewport.KeyMap)
-	return bindings
+	return []key.Binding{
+		m.viewport.KeyMap.PageDown,
+		m.viewport.KeyMap.PageUp,
+		m.viewport.KeyMap.HalfPageUp,
+		m.viewport.KeyMap.HalfPageDown,
+	}
 }
 
 func NewMessagesCmp(app *app.App) tea.Model {
 	s := spinner.New()
 	s.Spinner = spinner.Pulse
+	vp := viewport.New(0, 0)
+	vp.KeyMap.PageUp = messageKeys.PageUp
+	vp.KeyMap.PageDown = messageKeys.PageDown
+	vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
+	vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
 	return &messagesCmp{
 		app:           app,
-		writingMode:   true,
 		cachedContent: make(map[string]cacheItem),
-		viewport:      viewport.New(0, 0),
+		viewport:      vp,
 		spinner:       s,
 	}
 }

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

@@ -190,7 +190,6 @@ func (c *commandDialogCmp) View() string {
 		styles.BaseStyle.Width(maxWidth).Render(""),
 		styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)),
 		styles.BaseStyle.Width(maxWidth).Render(""),
-		styles.BaseStyle.Width(maxWidth).Padding(0, 1).Foreground(styles.ForgroundDim).Render("↑/k: up  ↓/j: down  enter: select  esc: cancel"),
 	)
 
 	return styles.BaseStyle.Padding(1, 2).
@@ -244,4 +243,3 @@ func NewCommandDialogCmp() CommandDialog {
 		selectedCommandID: "",
 	}
 }
-

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

@@ -62,7 +62,7 @@ func (h *helpCmp) render() string {
 	var (
 		pairs []string
 		width int
-		rows  = 14 - 2
+		rows  = 10 - 2
 	)
 	for i := 0; i < len(bindings); i += rows {
 		var (

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

@@ -46,8 +46,8 @@ func (k initDialogKeyMap) ShortHelp() []key.Binding {
 			key.WithHelp("enter", "confirm"),
 		),
 		key.NewBinding(
-			key.WithKeys("esc"),
-			key.WithHelp("esc", "cancel"),
+			key.WithKeys("esc", "q"),
+			key.WithHelp("esc/q", "cancel"),
 		),
 		key.NewBinding(
 			key.WithKeys("y", "n"),
@@ -114,6 +114,7 @@ func (m InitDialogCmp) View() string {
 		Padding(1, 1).
 		Render("Would you like to initialize this project?")
 
+	maxWidth = min(maxWidth, m.width-10)
 	yesStyle := styles.BaseStyle
 	noStyle := styles.BaseStyle
 
@@ -144,12 +145,6 @@ func (m InitDialogCmp) View() string {
 		Padding(1, 0).
 		Render(buttons)
 
-	help := styles.BaseStyle.
-		Width(maxWidth).
-		Padding(0, 1).
-		Foreground(styles.ForgroundDim).
-		Render("tab/←/→: toggle  y/n: yes/no  enter: confirm  esc: cancel")
-
 	content := lipgloss.JoinVertical(
 		lipgloss.Left,
 		title,
@@ -158,7 +153,6 @@ func (m InitDialogCmp) View() string {
 		question,
 		buttons,
 		styles.BaseStyle.Width(maxWidth).Render(""),
-		help,
 	)
 
 	return styles.BaseStyle.Padding(1, 2).

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

@@ -64,15 +64,15 @@ var permissionsKeys = permissionsMapping{
 	),
 	Allow: key.NewBinding(
 		key.WithKeys("a"),
-		key.WithHelp("a", "allow"),
+		key.WithHelp("a", "[a]llow"),
 	),
 	AllowSession: key.NewBinding(
-		key.WithKeys("A"),
-		key.WithHelp("A", "allow for session"),
+		key.WithKeys("s"),
+		key.WithHelp("s", "allow for [s]ession"),
 	),
 	Deny: key.NewBinding(
 		key.WithKeys("d"),
-		key.WithHelp("d", "deny"),
+		key.WithHelp("d", "[d]eny"),
 	),
 	Tab: key.NewBinding(
 		key.WithKeys("tab"),
@@ -375,9 +375,6 @@ func (p *permissionDialogCmp) render() string {
 		contentFinal = p.renderDefaultContent()
 	}
 
-	// Add help text
-	helpText := styles.BaseStyle.Width(p.width - 4).Padding(0, 1).Foreground(styles.ForgroundDim).Render("←/→/tab: switch options  a: allow  A: allow for session  d: deny  enter/space: confirm")
-	
 	content := lipgloss.JoinVertical(
 		lipgloss.Top,
 		title,
@@ -385,8 +382,7 @@ func (p *permissionDialogCmp) render() string {
 		headerContent,
 		contentFinal,
 		buttons,
-		styles.BaseStyle.Render(strings.Repeat(" ", p.width - 4)),
-		helpText,
+		styles.BaseStyle.Render(strings.Repeat(" ", p.width-4)),
 	)
 
 	return styles.BaseStyle.

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

@@ -122,6 +122,8 @@ func (s *sessionDialogCmp) View() string {
 		}
 	}
 
+	maxWidth = max(30, min(maxWidth, s.width-15)) // Limit width to avoid overflow
+
 	// Limit height to avoid taking up too much screen space
 	maxVisibleSessions := min(10, len(s.sessions))
 
@@ -169,7 +171,6 @@ func (s *sessionDialogCmp) View() string {
 		styles.BaseStyle.Width(maxWidth).Render(""),
 		styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
 		styles.BaseStyle.Width(maxWidth).Render(""),
-		styles.BaseStyle.Width(maxWidth).Padding(0, 1).Foreground(styles.ForgroundDim).Render("↑/k: up  ↓/j: down  enter: select  esc: cancel"),
 	)
 
 	return styles.BaseStyle.Padding(1, 2).
@@ -223,4 +224,3 @@ func NewSessionDialogCmp() SessionDialog {
 		selectedSessionID: "",
 	}
 }
-

internal/tui/page/chat.go 🔗

@@ -15,12 +15,11 @@ import (
 var ChatPage PageID = "chat"
 
 type chatPage struct {
-	app         *app.App
-	editor      layout.Container
-	messages    layout.Container
-	layout      layout.SplitPaneLayout
-	session     session.Session
-	editingMode bool
+	app      *app.App
+	editor   layout.Container
+	messages layout.Container
+	layout   layout.SplitPaneLayout
+	session  session.Session
 }
 
 type ChatKeyMap struct {
@@ -34,8 +33,8 @@ var keyMap = ChatKeyMap{
 		key.WithHelp("ctrl+n", "new session"),
 	),
 	Cancel: key.NewBinding(
-		key.WithKeys("ctrl+x"),
-		key.WithHelp("ctrl+x", "cancel"),
+		key.WithKeys("esc"),
+		key.WithHelp("esc", "cancel"),
 	),
 }
 
@@ -65,8 +64,6 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			}
 		}
 		p.session = msg
-	case chat.EditorFocusMsg:
-		p.editingMode = bool(msg)
 	case tea.KeyMsg:
 		switch {
 		case key.Matches(msg, keyMap.NewSession):
@@ -136,11 +133,7 @@ func (p *chatPage) View() string {
 
 func (p *chatPage) BindingKeys() []key.Binding {
 	bindings := layout.KeyMapToSlice(keyMap)
-	if p.editingMode {
-		bindings = append(bindings, p.editor.BindingKeys()...)
-	} else {
-		bindings = append(bindings, p.messages.BindingKeys()...)
-	}
+	bindings = append(bindings, p.messages.BindingKeys()...)
 	return bindings
 }
 
@@ -155,10 +148,9 @@ func NewChatPage(app *app.App) tea.Model {
 		layout.WithBorder(true, false, false, false),
 	)
 	return &chatPage{
-		app:         app,
-		editor:      editorContainer,
-		messages:    messagesContainer,
-		editingMode: true,
+		app:      app,
+		editor:   editorContainer,
+		messages: messagesContainer,
 		layout: layout.NewSplitPane(
 			layout.WithLeftPanel(messagesContainer),
 			layout.WithBottomPanel(editorContainer),

internal/tui/tui.go 🔗

@@ -30,7 +30,7 @@ type keyMap struct {
 var keys = keyMap{
 	Logs: key.NewBinding(
 		key.WithKeys("ctrl+l"),
-		key.WithHelp("ctrl+L", "logs"),
+		key.WithHelp("ctrl+l", "logs"),
 	),
 
 	Quit: key.NewBinding(
@@ -49,7 +49,7 @@ var keys = keyMap{
 
 	Commands: key.NewBinding(
 		key.WithKeys("ctrl+k"),
-		key.WithHelp("ctrl+K", "commands"),
+		key.WithHelp("ctrl+k", "commands"),
 	),
 }
 
@@ -95,8 +95,6 @@ type appModel struct {
 
 	showInitDialog bool
 	initDialog     dialog.InitDialogCmp
-
-	editingMode bool
 }
 
 func (a appModel) Init() tea.Cmd {
@@ -164,8 +162,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		a.initDialog.SetSize(msg.Width, msg.Height)
 
 		return a, tea.Batch(cmds...)
-	case chat.EditorFocusMsg:
-		a.editingMode = bool(msg)
 	// Status
 	case util.InfoMsg:
 		s, cmd := a.status.Update(msg)
@@ -360,7 +356,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			a.showHelp = !a.showHelp
 			return a, nil
 		case key.Matches(msg, helpEsc):
-			if !a.editingMode {
+			if a.app.CoderAgent.IsBusy() {
 				if a.showQuit {
 					return a, nil
 				}
@@ -477,7 +473,7 @@ func (a appModel) View() string {
 		)
 	}
 
-	if a.editingMode {
+	if !a.app.CoderAgent.IsBusy() {
 		a.status.SetHelpMsg("ctrl+? help")
 	} else {
 		a.status.SetHelpMsg("? help")
@@ -494,7 +490,7 @@ func (a appModel) View() string {
 		if a.currentPage == page.LogsPage {
 			bindings = append(bindings, logsKeyReturnKey)
 		}
-		if !a.editingMode {
+		if !a.app.CoderAgent.IsBusy() {
 			bindings = append(bindings, helpEsc)
 		}
 		a.help.SetBindings(bindings)
@@ -585,7 +581,6 @@ func New(app *app.App) tea.Model {
 		permissions:   dialog.NewPermissionDialogCmp(),
 		initDialog:    dialog.NewInitDialogCmp(),
 		app:           app,
-		editingMode:   true,
 		commands:      []dialog.Command{},
 		pages: map[page.PageID]tea.Model{
 			page.ChatPage: page.NewChatPage(app),