Detailed changes
@@ -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))
}
@@ -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))
@@ -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
@@ -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
@@ -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,
}
@@ -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)
}
@@ -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
@@ -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: "",
}
-}
+}
+
@@ -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...)
}
@@ -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)