small fixes

Kujtim Hoxha created

Change summary

internal/diff/patch.go                       | 33 +++++++-------
internal/llm/agent/agent.go                  |  2 
internal/llm/tools/edit.go                   |  6 +-
internal/tui/components/chat/editor.go       |  9 +++
internal/tui/components/chat/list.go         | 49 ++++++++++++++++-----
internal/tui/components/chat/message.go      |  1 
internal/tui/components/dialog/permission.go | 22 +++++----
internal/tui/components/dialog/session.go    | 48 +++++++++++----------
internal/tui/page/chat.go                    |  7 ---
internal/tui/tui.go                          |  4 +
10 files changed, 106 insertions(+), 75 deletions(-)

Detailed changes

internal/diff/patch.go 🔗

@@ -91,11 +91,9 @@ func (p *Parser) isDone(prefixes []string) bool {
 	if p.index >= len(p.lines) {
 		return true
 	}
-	if prefixes != nil {
-		for _, prefix := range prefixes {
-			if strings.HasPrefix(p.lines[p.index], prefix) {
-				return true
-			}
+	for _, prefix := range prefixes {
+		if strings.HasPrefix(p.lines[p.index], prefix) {
+			return true
 		}
 	}
 	return false
@@ -219,7 +217,7 @@ func (p *Parser) parseUpdateFile(text string) (PatchAction, error) {
 			sectionStr = p.lines[p.index]
 			p.index++
 		}
-		if !(defStr != "" || sectionStr != "" || index == 0) {
+		if defStr == "" && sectionStr == "" && index != 0 {
 			return action, NewDiffError(fmt.Sprintf("Invalid Line:\n%s", p.lines[p.index]))
 		}
 		if strings.TrimSpace(defStr) != "" {
@@ -433,12 +431,13 @@ func peekNextSection(lines []string, initialIndex int) ([]string, []Chunk, int,
 			delLines = make([]string, 0, 8)
 			insLines = make([]string, 0, 8)
 		}
-		if mode == "delete" {
+		switch mode {
+		case "delete":
 			delLines = append(delLines, line)
 			old = append(old, line)
-		} else if mode == "add" {
+		case "add":
 			insLines = append(insLines, line)
-		} else {
+		default:
 			old = append(old, line)
 		}
 	}
@@ -513,7 +512,7 @@ func IdentifyFilesAdded(text string) []string {
 
 func getUpdatedFile(text string, action PatchAction, path string) (string, error) {
 	if action.Type != ActionUpdate {
-		return "", errors.New("Expected UPDATE action")
+		return "", errors.New("expected UPDATE action")
 	}
 	origLines := strings.Split(text, "\n")
 	destLines := make([]string, 0, len(origLines)) // Preallocate with capacity
@@ -543,18 +542,19 @@ func getUpdatedFile(text string, action PatchAction, path string) (string, error
 func PatchToCommit(patch Patch, orig map[string]string) (Commit, error) {
 	commit := Commit{Changes: make(map[string]FileChange, len(patch.Actions))}
 	for pathKey, action := range patch.Actions {
-		if action.Type == ActionDelete {
+		switch action.Type {
+		case ActionDelete:
 			oldContent := orig[pathKey]
 			commit.Changes[pathKey] = FileChange{
 				Type:       ActionDelete,
 				OldContent: &oldContent,
 			}
-		} else if action.Type == ActionAdd {
+		case ActionAdd:
 			commit.Changes[pathKey] = FileChange{
 				Type:       ActionAdd,
 				NewContent: action.NewFile,
 			}
-		} else if action.Type == ActionUpdate {
+		case ActionUpdate:
 			newContent, err := getUpdatedFile(orig[pathKey], action, pathKey)
 			if err != nil {
 				return Commit{}, err
@@ -619,18 +619,19 @@ func LoadFiles(paths []string, openFn func(string) (string, error)) (map[string]
 
 func ApplyCommit(commit Commit, writeFn func(string, string) error, removeFn func(string) error) error {
 	for p, change := range commit.Changes {
-		if change.Type == ActionDelete {
+		switch change.Type {
+		case ActionDelete:
 			if err := removeFn(p); err != nil {
 				return err
 			}
-		} else if change.Type == ActionAdd {
+		case ActionAdd:
 			if change.NewContent == nil {
 				return NewDiffError(fmt.Sprintf("Add action for %s has nil new_content", p))
 			}
 			if err := writeFn(p, *change.NewContent); err != nil {
 				return err
 			}
-		} else if change.Type == ActionUpdate {
+		case ActionUpdate:
 			if change.NewContent == nil {
 				return NewDiffError(fmt.Sprintf("Update action for %s has nil new_content", p))
 			}

internal/llm/agent/agent.go 🔗

@@ -221,6 +221,8 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
 		agentMessage, toolResults, err := a.streamAndHandleEvents(ctx, sessionID, msgHistory)
 		if err != nil {
 			if errors.Is(err, context.Canceled) {
+				agentMessage.AddFinish(message.FinishReasonCanceled)
+				a.messages.Update(context.Background(), agentMessage)
 				return a.err(ErrRequestCancelled)
 			}
 			return a.err(fmt.Errorf("failed to process events: %w", err))

internal/llm/tools/edit.go 🔗

@@ -141,20 +141,20 @@ func (e *editTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
 	if params.OldString == "" {
 		response, err = e.createNewFile(ctx, params.FilePath, params.NewString)
 		if err != nil {
-			return response, nil
+			return response, err
 		}
 	}
 
 	if params.NewString == "" {
 		response, err = e.deleteContent(ctx, params.FilePath, params.OldString)
 		if err != nil {
-			return response, nil
+			return response, err
 		}
 	}
 
 	response, err = e.replaceContent(ctx, params.FilePath, params.OldString, params.NewString)
 	if err != nil {
-		return response, nil
+		return response, err
 	}
 	if response.IsError {
 		// Return early if there was an error during content replacement

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

@@ -21,6 +21,8 @@ type editorCmp struct {
 	textarea textarea.Model
 }
 
+type FocusEditorMsg bool
+
 type focusedEditorKeyMaps struct {
 	Send       key.Binding
 	OpenEditor key.Binding
@@ -112,7 +114,6 @@ func (m *editorCmp) send() tea.Cmd {
 		util.CmdHandler(SendMsg{
 			Text: value,
 		}),
-		util.CmdHandler(EditorFocusMsg(false)),
 	)
 }
 
@@ -124,9 +125,13 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			m.session = msg
 		}
 		return m, nil
+	case FocusEditorMsg:
+		if msg {
+			m.textarea.Focus()
+			return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
+		}
 	case tea.KeyMsg:
 		if key.Matches(msg, focusedKeyMaps.OpenEditor) {
-			m.textarea.Blur()
 			return m, openEditor()
 		}
 		// if the key does not match any binding, return

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

@@ -22,6 +22,10 @@ import (
 	"github.com/kujtimiihoxha/opencode/internal/tui/util"
 )
 
+type cacheItem struct {
+	width   int
+	content []uiMessage
+}
 type messagesCmp struct {
 	app           *app.App
 	width, height int
@@ -32,8 +36,9 @@ type messagesCmp struct {
 	uiMessages    []uiMessage
 	currentMsgID  string
 	mutex         sync.Mutex
-	cachedContent map[string][]uiMessage
+	cachedContent map[string]cacheItem
 	spinner       spinner.Model
+	lastUpdate    time.Time
 	rendering     bool
 }
 type renderFinishedMsg struct{}
@@ -44,6 +49,8 @@ func (m *messagesCmp) Init() tea.Cmd {
 
 func (m *messagesCmp) preloadSessions() tea.Cmd {
 	return func() tea.Msg {
+		m.mutex.Lock()
+		defer m.mutex.Unlock()
 		sessions, err := m.app.Sessions.List(context.Background())
 		if err != nil {
 			return util.ReportError(err)()
@@ -67,13 +74,13 @@ func (m *messagesCmp) preloadSessions() tea.Cmd {
 		}
 		logging.Debug("preloaded sessions")
 
-		return nil
+		return func() tea.Msg {
+			return renderFinishedMsg{}
+		}
 	}
 }
 
 func (m *messagesCmp) cacheSessionMessages(messages []message.Message, width int) {
-	m.mutex.Lock()
-	defer m.mutex.Unlock()
 	pos := 0
 	if m.width == 0 {
 		return
@@ -87,7 +94,10 @@ func (m *messagesCmp) cacheSessionMessages(messages []message.Message, width int
 				width,
 				pos,
 			)
-			m.cachedContent[msg.ID] = []uiMessage{userMsg}
+			m.cachedContent[msg.ID] = cacheItem{
+				width:   width,
+				content: []uiMessage{userMsg},
+			}
 			pos += userMsg.height + 1 // + 1 for spacing
 		case message.Assistant:
 			assistantMessages := renderAssistantMessage(
@@ -102,7 +112,10 @@ func (m *messagesCmp) cacheSessionMessages(messages []message.Message, width int
 			for _, msg := range assistantMessages {
 				pos += msg.height + 1 // + 1 for spacing
 			}
-			m.cachedContent[msg.ID] = assistantMessages
+			m.cachedContent[msg.ID] = cacheItem{
+				width:   width,
+				content: assistantMessages,
+			}
 		}
 	}
 }
@@ -223,8 +236,8 @@ func (m *messagesCmp) renderView() {
 	for inx, msg := range m.messages {
 		switch msg.Role {
 		case message.User:
-			if messages, ok := m.cachedContent[msg.ID]; ok {
-				m.uiMessages = append(m.uiMessages, messages...)
+			if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
+				m.uiMessages = append(m.uiMessages, cache.content...)
 				continue
 			}
 			userMsg := renderUserMessage(
@@ -234,11 +247,14 @@ func (m *messagesCmp) renderView() {
 				pos,
 			)
 			m.uiMessages = append(m.uiMessages, userMsg)
-			m.cachedContent[msg.ID] = []uiMessage{userMsg}
+			m.cachedContent[msg.ID] = cacheItem{
+				width:   m.width,
+				content: []uiMessage{userMsg},
+			}
 			pos += userMsg.height + 1 // + 1 for spacing
 		case message.Assistant:
-			if messages, ok := m.cachedContent[msg.ID]; ok {
-				m.uiMessages = append(m.uiMessages, messages...)
+			if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
+				m.uiMessages = append(m.uiMessages, cache.content...)
 				continue
 			}
 			assistantMessages := renderAssistantMessage(
@@ -254,7 +270,10 @@ func (m *messagesCmp) renderView() {
 				m.uiMessages = append(m.uiMessages, msg)
 				pos += msg.height + 1 // + 1 for spacing
 			}
-			m.cachedContent[msg.ID] = assistantMessages
+			m.cachedContent[msg.ID] = cacheItem{
+				width:   m.width,
+				content: assistantMessages,
+			}
 		}
 	}
 
@@ -418,6 +437,10 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
 	m.height = height
 	m.viewport.Width = width
 	m.viewport.Height = height - 2
+	for _, msg := range m.messages {
+		delete(m.cachedContent, msg.ID)
+	}
+	m.uiMessages = make([]uiMessage, 0)
 	m.renderView()
 	return m.preloadSessions()
 }
@@ -456,7 +479,7 @@ func NewMessagesCmp(app *app.App) tea.Model {
 	return &messagesCmp{
 		app:           app,
 		writingMode:   true,
-		cachedContent: make(map[string][]uiMessage),
+		cachedContent: make(map[string]cacheItem),
 		viewport:      viewport.New(0, 0),
 		spinner:       s,
 	}

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

@@ -389,6 +389,7 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
 		errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " "))
 		errContent = ansi.Truncate(errContent, width-1, "...")
 		return styles.BaseStyle.
+			Width(width).
 			Foreground(styles.Error).
 			Render(errContent)
 	}

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

@@ -40,7 +40,8 @@ type PermissionDialogCmp interface {
 }
 
 type permissionsMapping struct {
-	LeftRight    key.Binding
+	Left         key.Binding
+	Right        key.Binding
 	EnterSpace   key.Binding
 	Allow        key.Binding
 	AllowSession key.Binding
@@ -49,9 +50,13 @@ type permissionsMapping struct {
 }
 
 var permissionsKeys = permissionsMapping{
-	LeftRight: key.NewBinding(
-		key.WithKeys("left", "right"),
-		key.WithHelp("←/→", "switch options"),
+	Left: key.NewBinding(
+		key.WithKeys("left"),
+		key.WithHelp("←", "switch options"),
+	),
+	Right: key.NewBinding(
+		key.WithKeys("right"),
+		key.WithHelp("→", "switch options"),
 	),
 	EnterSpace: key.NewBinding(
 		key.WithKeys("enter", " "),
@@ -104,21 +109,18 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		p.diffCache = make(map[string]string)
 	case tea.KeyMsg:
 		switch {
-		case key.Matches(msg, permissionsKeys.LeftRight) || key.Matches(msg, permissionsKeys.Tab):
-			// Change selected option
+		case key.Matches(msg, permissionsKeys.Right) || key.Matches(msg, permissionsKeys.Tab):
 			p.selectedOption = (p.selectedOption + 1) % 3
 			return p, nil
+		case key.Matches(msg, permissionsKeys.Left):
+			p.selectedOption = (p.selectedOption + 2) % 3
 		case key.Matches(msg, permissionsKeys.EnterSpace):
-			// Select current option
 			return p, p.selectCurrentOption()
 		case key.Matches(msg, permissionsKeys.Allow):
-			// Select Allow
 			return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllow, Permission: p.permission})
 		case key.Matches(msg, permissionsKeys.AllowSession):
-			// Select Allow for session
 			return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllowForSession, Permission: p.permission})
 		case key.Matches(msg, permissionsKeys.Deny):
-			// Select Deny
 			return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionDeny, Permission: p.permission})
 		default:
 			// Pass other keys to viewport

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

@@ -27,20 +27,20 @@ type SessionDialog interface {
 }
 
 type sessionDialogCmp struct {
-	sessions     []session.Session
-	selectedIdx  int
-	width        int
-	height       int
+	sessions          []session.Session
+	selectedIdx       int
+	width             int
+	height            int
 	selectedSessionID string
 }
 
 type sessionKeyMap struct {
-	Up         key.Binding
-	Down       key.Binding
-	Enter      key.Binding
-	Escape     key.Binding
-	J          key.Binding
-	K          key.Binding
+	Up     key.Binding
+	Down   key.Binding
+	Enter  key.Binding
+	Escape key.Binding
+	J      key.Binding
+	K      key.Binding
 }
 
 var sessionKeys = sessionKeyMap{
@@ -128,7 +128,7 @@ func (s *sessionDialogCmp) View() string {
 	// Build the session list
 	sessionItems := make([]string, 0, maxVisibleSessions)
 	startIdx := 0
-	
+
 	// If we have more sessions than can be displayed, adjust the start index
 	if len(s.sessions) > maxVisibleSessions {
 		// Center the selected item when possible
@@ -145,30 +145,31 @@ func (s *sessionDialogCmp) View() string {
 	for i := startIdx; i < endIdx; i++ {
 		sess := s.sessions[i]
 		itemStyle := styles.BaseStyle.Width(maxWidth)
-		
+
 		if i == s.selectedIdx {
 			itemStyle = itemStyle.
 				Background(styles.PrimaryColor).
 				Foreground(styles.Background).
 				Bold(true)
 		}
-		
+
 		sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title))
 	}
 
 	title := styles.BaseStyle.
 		Foreground(styles.PrimaryColor).
 		Bold(true).
+		Width(maxWidth).
 		Padding(0, 1).
 		Render("Switch Session")
 
 	content := lipgloss.JoinVertical(
 		lipgloss.Left,
 		title,
-		styles.BaseStyle.Render(""),
-		lipgloss.JoinVertical(lipgloss.Left, sessionItems...),
-		styles.BaseStyle.Render(""),
-		styles.BaseStyle.Foreground(styles.ForgroundDim).Render("↑/k: up  ↓/j: down  enter: select  esc: cancel"),
+		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).
@@ -185,7 +186,7 @@ func (s *sessionDialogCmp) BindingKeys() []key.Binding {
 
 func (s *sessionDialogCmp) SetSessions(sessions []session.Session) {
 	s.sessions = sessions
-	
+
 	// If we have a selected session ID, find its index
 	if s.selectedSessionID != "" {
 		for i, sess := range sessions {
@@ -195,14 +196,14 @@ func (s *sessionDialogCmp) SetSessions(sessions []session.Session) {
 			}
 		}
 	}
-	
+
 	// Default to first session if selected not found
 	s.selectedIdx = 0
 }
 
 func (s *sessionDialogCmp) SetSelectedSession(sessionID string) {
 	s.selectedSessionID = sessionID
-	
+
 	// Update the selected index if sessions are already loaded
 	if len(s.sessions) > 0 {
 		for i, sess := range s.sessions {
@@ -217,8 +218,9 @@ func (s *sessionDialogCmp) SetSelectedSession(sessionID string) {
 // NewSessionDialogCmp creates a new session switching dialog
 func NewSessionDialogCmp() SessionDialog {
 	return &sessionDialogCmp{
-		sessions:         []session.Session{},
-		selectedIdx:      0,
+		sessions:          []session.Session{},
+		selectedIdx:       0,
 		selectedSessionID: "",
 	}
-}
+}
+

internal/tui/page/chat.go 🔗

@@ -43,13 +43,6 @@ func (p *chatPage) Init() tea.Cmd {
 	cmds := []tea.Cmd{
 		p.layout.Init(),
 	}
-
-	sessions, _ := p.app.Sessions.List(context.Background())
-	if len(sessions) > 0 {
-		p.session = sessions[0]
-		cmd := p.setSidebar()
-		cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(p.session)), cmd)
-	}
 	return tea.Batch(cmds...)
 }
 

internal/tui/tui.go 🔗

@@ -163,6 +163,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		a.showPermissions = true
 		return a, a.permissions.SetPermissions(msg.Payload)
 	case dialog.PermissionResponseMsg:
+		var cmd tea.Cmd
 		switch msg.Action {
 		case dialog.PermissionAllow:
 			a.app.Permissions.Grant(msg.Permission)
@@ -170,9 +171,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			a.app.Permissions.GrantPersistant(msg.Permission)
 		case dialog.PermissionDeny:
 			a.app.Permissions.Deny(msg.Permission)
+			cmd = util.CmdHandler(chat.FocusEditorMsg(true))
 		}
 		a.showPermissions = false
-		return a, nil
+		return a, cmd
 
 	case page.PageChangeMsg:
 		return a, a.moveToPage(msg.ID)