on main help sign in
small fixes
commit72afeb9f54cee8e248093a52ac0779441c79aea3 authorKujtim Hoxha <kujtimii.h@gmail.com> dateFri 18 Apr 2025, 21:24 (2 years ago) parent333ea6e
10 files changed+106−75
internal/diff/patch.go+17−16 internal/llm/agent/agent.go+2 internal/llm/tools/edit.go+3−3 internal/tui/components/chat/editor.go+7−2 internal/tui/components/chat/list.go+36−13 internal/tui/components/chat/message.go+1 internal/tui/components/dialog/permission.go+12−10 internal/tui/components/dialog/session.go+25−23 internal/tui/page/chat.go−7 internal/tui/tui.go+3−1
internal/diff/patch.go+17−16
@@ -91,11 +91,9 @@ func (p *Parser) isDone(prefixes []string) bool {
9191 if p.index >= len(p.lines) {
9292 return true
9393 }
94− if prefixes != nil {
95− for _, prefix := range prefixes {
96− if strings.HasPrefix(p.lines[p.index], prefix) {
97− return true
98− }
94+ for _, prefix := range prefixes {
95+ if strings.HasPrefix(p.lines[p.index], prefix) {
96+ return true
9997 }
10098 }
10199 return false
@@ -219,7 +217,7 @@ func (p *Parser) parseUpdateFile(text string) (PatchAction, error) {
219217 sectionStr = p.lines[p.index]
220218 p.index++
221219 }
222− if !(defStr != "" || sectionStr != "" || index == 0) {
220+ if defStr == "" && sectionStr == "" && index != 0 {
223221 return action, NewDiffError(fmt.Sprintf("Invalid Line:\n%s", p.lines[p.index]))
224222 }
225223 if strings.TrimSpace(defStr) != "" {
@@ -433,12 +431,13 @@ func peekNextSection(lines []string, initialIndex int) ([]string, []Chunk, int,
433431 delLines = make([]string, 0, 8)
434432 insLines = make([]string, 0, 8)
435433 }
436− if mode == "delete" {
434+ switch mode {
435+ case "delete":
437436 delLines = append(delLines, line)
438437 old = append(old, line)
439− } else if mode == "add" {
438+ case "add":
440439 insLines = append(insLines, line)
441− } else {
440+ default:
442441 old = append(old, line)
443442 }
444443 }
@@ -513,7 +512,7 @@ func IdentifyFilesAdded(text string) []string {
513512
514513func getUpdatedFile(text string, action PatchAction, path string) (string, error) {
515514 if action.Type != ActionUpdate {
516− return "", errors.New("Expected UPDATE action")
515+ return "", errors.New("expected UPDATE action")
517516 }
518517 origLines := strings.Split(text, "\n")
519518 destLines := make([]string, 0, len(origLines)) // Preallocate with capacity
@@ -543,18 +542,19 @@ func getUpdatedFile(text string, action PatchAction, path string) (string, error
543542func PatchToCommit(patch Patch, orig map[string]string) (Commit, error) {
544543 commit := Commit{Changes: make(map[string]FileChange, len(patch.Actions))}
545544 for pathKey, action := range patch.Actions {
546− if action.Type == ActionDelete {
545+ switch action.Type {
546+ case ActionDelete:
547547 oldContent := orig[pathKey]
548548 commit.Changes[pathKey] = FileChange{
549549 Type: ActionDelete,
550550 OldContent: &oldContent,
551551 }
552− } else if action.Type == ActionAdd {
552+ case ActionAdd:
553553 commit.Changes[pathKey] = FileChange{
554554 Type: ActionAdd,
555555 NewContent: action.NewFile,
556556 }
557− } else if action.Type == ActionUpdate {
557+ case ActionUpdate:
558558 newContent, err := getUpdatedFile(orig[pathKey], action, pathKey)
559559 if err != nil {
560560 return Commit{}, err
@@ -619,18 +619,19 @@ func LoadFiles(paths []string, openFn func(string) (string, error)) (map[string]
619619
620620func ApplyCommit(commit Commit, writeFn func(string, string) error, removeFn func(string) error) error {
621621 for p, change := range commit.Changes {
622− if change.Type == ActionDelete {
622+ switch change.Type {
623+ case ActionDelete:
623624 if err := removeFn(p); err != nil {
624625 return err
625626 }
626− } else if change.Type == ActionAdd {
627+ case ActionAdd:
627628 if change.NewContent == nil {
628629 return NewDiffError(fmt.Sprintf("Add action for %s has nil new_content", p))
629630 }
630631 if err := writeFn(p, *change.NewContent); err != nil {
631632 return err
632633 }
633− } else if change.Type == ActionUpdate {
634+ case ActionUpdate:
634635 if change.NewContent == nil {
635636 return NewDiffError(fmt.Sprintf("Update action for %s has nil new_content", p))
636637 }
internal/llm/agent/agent.go+2
@@ -221,6 +221,8 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
221221 agentMessage, toolResults, err := a.streamAndHandleEvents(ctx, sessionID, msgHistory)
222222 if err != nil {
223223 if errors.Is(err, context.Canceled) {
224+ agentMessage.AddFinish(message.FinishReasonCanceled)
225+ a.messages.Update(context.Background(), agentMessage)
224226 return a.err(ErrRequestCancelled)
225227 }
226228 return a.err(fmt.Errorf("failed to process events: %w", err))
internal/llm/tools/edit.go+3−3
@@ -141,20 +141,20 @@ func (e *editTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
141141 if params.OldString == "" {
142142 response, err = e.createNewFile(ctx, params.FilePath, params.NewString)
143143 if err != nil {
144− return response, nil
144+ return response, err
145145 }
146146 }
147147
148148 if params.NewString == "" {
149149 response, err = e.deleteContent(ctx, params.FilePath, params.OldString)
150150 if err != nil {
151− return response, nil
151+ return response, err
152152 }
153153 }
154154
155155 response, err = e.replaceContent(ctx, params.FilePath, params.OldString, params.NewString)
156156 if err != nil {
157− return response, nil
157+ return response, err
158158 }
159159 if response.IsError {
160160 // Return early if there was an error during content replacement
internal/tui/components/chat/editor.go+7−2
@@ -21,6 +21,8 @@ type editorCmp struct {
2121 textarea textarea.Model
2222}
2323
24+type FocusEditorMsg bool
25+
2426type focusedEditorKeyMaps struct {
2527 Send key.Binding
2628 OpenEditor key.Binding
@@ -112,7 +114,6 @@ func (m *editorCmp) send() tea.Cmd {
112114 util.CmdHandler(SendMsg{
113115 Text: value,
114116 }),
115− util.CmdHandler(EditorFocusMsg(false)),
116117 )
117118}
118119
@@ -124,9 +125,13 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
124125 m.session = msg
125126 }
126127 return m, nil
128+ case FocusEditorMsg:
129+ if msg {
130+ m.textarea.Focus()
131+ return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
132+ }
127133 case tea.KeyMsg:
128134 if key.Matches(msg, focusedKeyMaps.OpenEditor) {
129− m.textarea.Blur()
130135 return m, openEditor()
131136 }
132137 // if the key does not match any binding, return
internal/tui/components/chat/list.go+36−13
@@ -22,6 +22,10 @@ import (
2222 "github.com/kujtimiihoxha/opencode/internal/tui/util"
2323)
2424
25+type cacheItem struct {
26+ width int
27+ content []uiMessage
28+}
2529type messagesCmp struct {
2630 app *app.App
2731 width, height int
@@ -32,8 +36,9 @@ type messagesCmp struct {
3236 uiMessages []uiMessage
3337 currentMsgID string
3438 mutex sync.Mutex
35− cachedContent map[string][]uiMessage
39+ cachedContent map[string]cacheItem
3640 spinner spinner.Model
41+ lastUpdate time.Time
3742 rendering bool
3843}
3944type renderFinishedMsg struct{}
@@ -44,6 +49,8 @@ func (m *messagesCmp) Init() tea.Cmd {
4449
4550func (m *messagesCmp) preloadSessions() tea.Cmd {
4651 return func() tea.Msg {
52+ m.mutex.Lock()
53+ defer m.mutex.Unlock()
4754 sessions, err := m.app.Sessions.List(context.Background())
4855 if err != nil {
4956 return util.ReportError(err)()
@@ -67,13 +74,13 @@ func (m *messagesCmp) preloadSessions() tea.Cmd {
6774 }
6875 logging.Debug("preloaded sessions")
6976
70− return nil
77+ return func() tea.Msg {
78+ return renderFinishedMsg{}
79+ }
7180 }
7281}
7382
7483func (m *messagesCmp) cacheSessionMessages(messages []message.Message, width int) {
75− m.mutex.Lock()
76− defer m.mutex.Unlock()
7784 pos := 0
7885 if m.width == 0 {
7986 return
@@ -87,7 +94,10 @@ func (m *messagesCmp) cacheSessionMessages(messages []message.Message, width int
8794 width,
8895 pos,
8996 )
90− m.cachedContent[msg.ID] = []uiMessage{userMsg}
97+ m.cachedContent[msg.ID] = cacheItem{
98+ width: width,
99+ content: []uiMessage{userMsg},
100+ }
91101 pos += userMsg.height + 1 // + 1 for spacing
92102 case message.Assistant:
93103 assistantMessages := renderAssistantMessage(
@@ -102,7 +112,10 @@ func (m *messagesCmp) cacheSessionMessages(messages []message.Message, width int
102112 for _, msg := range assistantMessages {
103113 pos += msg.height + 1 // + 1 for spacing
104114 }
105− m.cachedContent[msg.ID] = assistantMessages
115+ m.cachedContent[msg.ID] = cacheItem{
116+ width: width,
117+ content: assistantMessages,
118+ }
106119 }
107120 }
108121}
@@ -223,8 +236,8 @@ func (m *messagesCmp) renderView() {
223236 for inx, msg := range m.messages {
224237 switch msg.Role {
225238 case message.User:
226− if messages, ok := m.cachedContent[msg.ID]; ok {
227− m.uiMessages = append(m.uiMessages, messages...)
239+ if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
240+ m.uiMessages = append(m.uiMessages, cache.content...)
228241 continue
229242 }
230243 userMsg := renderUserMessage(
@@ -234,11 +247,14 @@ func (m *messagesCmp) renderView() {
234247 pos,
235248 )
236249 m.uiMessages = append(m.uiMessages, userMsg)
237− m.cachedContent[msg.ID] = []uiMessage{userMsg}
250+ m.cachedContent[msg.ID] = cacheItem{
251+ width: m.width,
252+ content: []uiMessage{userMsg},
253+ }
238254 pos += userMsg.height + 1 // + 1 for spacing
239255 case message.Assistant:
240− if messages, ok := m.cachedContent[msg.ID]; ok {
241− m.uiMessages = append(m.uiMessages, messages...)
256+ if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
257+ m.uiMessages = append(m.uiMessages, cache.content...)
242258 continue
243259 }
244260 assistantMessages := renderAssistantMessage(
@@ -254,7 +270,10 @@ func (m *messagesCmp) renderView() {
254270 m.uiMessages = append(m.uiMessages, msg)
255271 pos += msg.height + 1 // + 1 for spacing
256272 }
257− m.cachedContent[msg.ID] = assistantMessages
273+ m.cachedContent[msg.ID] = cacheItem{
274+ width: m.width,
275+ content: assistantMessages,
276+ }
258277 }
259278 }
260279
@@ -418,6 +437,10 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
418437 m.height = height
419438 m.viewport.Width = width
420439 m.viewport.Height = height - 2
440+ for _, msg := range m.messages {
441+ delete(m.cachedContent, msg.ID)
442+ }
443+ m.uiMessages = make([]uiMessage, 0)
421444 m.renderView()
422445 return m.preloadSessions()
423446}
@@ -456,7 +479,7 @@ func NewMessagesCmp(app *app.App) tea.Model {
456479 return &messagesCmp{
457480 app: app,
458481 writingMode: true,
459− cachedContent: make(map[string][]uiMessage),
482+ cachedContent: make(map[string]cacheItem),
460483 viewport: viewport.New(0, 0),
461484 spinner: s,
462485 }
internal/tui/components/chat/message.go+1
@@ -389,6 +389,7 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
389389 errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " "))
390390 errContent = ansi.Truncate(errContent, width-1, "...")
391391 return styles.BaseStyle.
392+ Width(width).
392393 Foreground(styles.Error).
393394 Render(errContent)
394395 }
internal/tui/components/dialog/permission.go+12−10
@@ -40,7 +40,8 @@ type PermissionDialogCmp interface {
4040}
4141
4242type permissionsMapping struct {
43− LeftRight key.Binding
43+ Left key.Binding
44+ Right key.Binding
4445 EnterSpace key.Binding
4546 Allow key.Binding
4647 AllowSession key.Binding
@@ -49,9 +50,13 @@ type permissionsMapping struct {
4950}
5051
5152var permissionsKeys = permissionsMapping{
52− LeftRight: key.NewBinding(
53− key.WithKeys("left", "right"),
54− key.WithHelp("←/→", "switch options"),
53+ Left: key.NewBinding(
54+ key.WithKeys("left"),
55+ key.WithHelp("←", "switch options"),
56+ ),
57+ Right: key.NewBinding(
58+ key.WithKeys("right"),
59+ key.WithHelp("→", "switch options"),
5560 ),
5661 EnterSpace: key.NewBinding(
5762 key.WithKeys("enter", " "),
@@ -104,21 +109,18 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
104109 p.diffCache = make(map[string]string)
105110 case tea.KeyMsg:
106111 switch {
107− case key.Matches(msg, permissionsKeys.LeftRight) || key.Matches(msg, permissionsKeys.Tab):
108− // Change selected option
112+ case key.Matches(msg, permissionsKeys.Right) || key.Matches(msg, permissionsKeys.Tab):
109113 p.selectedOption = (p.selectedOption + 1) % 3
110114 return p, nil
115+ case key.Matches(msg, permissionsKeys.Left):
116+ p.selectedOption = (p.selectedOption + 2) % 3
111117 case key.Matches(msg, permissionsKeys.EnterSpace):
112− // Select current option
113118 return p, p.selectCurrentOption()
114119 case key.Matches(msg, permissionsKeys.Allow):
115− // Select Allow
116120 return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllow, Permission: p.permission})
117121 case key.Matches(msg, permissionsKeys.AllowSession):
118− // Select Allow for session
119122 return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllowForSession, Permission: p.permission})
120123 case key.Matches(msg, permissionsKeys.Deny):
121− // Select Deny
122124 return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionDeny, Permission: p.permission})
123125 default:
124126 // Pass other keys to viewport
internal/tui/components/dialog/session.go+25−23
@@ -27,20 +27,20 @@ type SessionDialog interface {
2727}
2828
2929type sessionDialogCmp struct {
30− sessions []session.Session
31− selectedIdx int
32− width int
33− height int
30+ sessions []session.Session
31+ selectedIdx int
32+ width int
33+ height int
3434 selectedSessionID string
3535}
3636
3737type sessionKeyMap struct {
38− Up key.Binding
39− Down key.Binding
40− Enter key.Binding
41− Escape key.Binding
42− J key.Binding
43− K key.Binding
38+ Up key.Binding
39+ Down key.Binding
40+ Enter key.Binding
41+ Escape key.Binding
42+ J key.Binding
43+ K key.Binding
4444}
4545
4646var sessionKeys = sessionKeyMap{
@@ -128,7 +128,7 @@ func (s *sessionDialogCmp) View() string {
128128 // Build the session list
129129 sessionItems := make([]string, 0, maxVisibleSessions)
130130 startIdx := 0
131−
131+
132132 // If we have more sessions than can be displayed, adjust the start index
133133 if len(s.sessions) > maxVisibleSessions {
134134 // Center the selected item when possible
@@ -145,30 +145,31 @@ func (s *sessionDialogCmp) View() string {
145145 for i := startIdx; i < endIdx; i++ {
146146 sess := s.sessions[i]
147147 itemStyle := styles.BaseStyle.Width(maxWidth)
148−
148+
149149 if i == s.selectedIdx {
150150 itemStyle = itemStyle.
151151 Background(styles.PrimaryColor).
152152 Foreground(styles.Background).
153153 Bold(true)
154154 }
155−
155+
156156 sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title))
157157 }
158158
159159 title := styles.BaseStyle.
160160 Foreground(styles.PrimaryColor).
161161 Bold(true).
162+ Width(maxWidth).
162163 Padding(0, 1).
163164 Render("Switch Session")
164165
165166 content := lipgloss.JoinVertical(
166167 lipgloss.Left,
167168 title,
168− styles.BaseStyle.Render(""),
169− lipgloss.JoinVertical(lipgloss.Left, sessionItems...),
170− styles.BaseStyle.Render(""),
171− styles.BaseStyle.Foreground(styles.ForgroundDim).Render("↑/k: up ↓/j: down enter: select esc: cancel"),
169+ styles.BaseStyle.Width(maxWidth).Render(""),
170+ styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
171+ styles.BaseStyle.Width(maxWidth).Render(""),
172+ styles.BaseStyle.Width(maxWidth).Padding(0, 1).Foreground(styles.ForgroundDim).Render("↑/k: up ↓/j: down enter: select esc: cancel"),
172173 )
173174
174175 return styles.BaseStyle.Padding(1, 2).
@@ -185,7 +186,7 @@ func (s *sessionDialogCmp) BindingKeys() []key.Binding {
185186
186187func (s *sessionDialogCmp) SetSessions(sessions []session.Session) {
187188 s.sessions = sessions
188−
189+
189190 // If we have a selected session ID, find its index
190191 if s.selectedSessionID != "" {
191192 for i, sess := range sessions {
@@ -195,14 +196,14 @@ func (s *sessionDialogCmp) SetSessions(sessions []session.Session) {
195196 }
196197 }
197198 }
198−
199+
199200 // Default to first session if selected not found
200201 s.selectedIdx = 0
201202}
202203
203204func (s *sessionDialogCmp) SetSelectedSession(sessionID string) {
204205 s.selectedSessionID = sessionID
205−
206+
206207 // Update the selected index if sessions are already loaded
207208 if len(s.sessions) > 0 {
208209 for i, sess := range s.sessions {
@@ -217,8 +218,9 @@ func (s *sessionDialogCmp) SetSelectedSession(sessionID string) {
217218// NewSessionDialogCmp creates a new session switching dialog
218219func NewSessionDialogCmp() SessionDialog {
219220 return &sessionDialogCmp{
220− sessions: []session.Session{},
221− selectedIdx: 0,
221+ sessions: []session.Session{},
222+ selectedIdx: 0,
222223 selectedSessionID: "",
223224 }
224−}
225+}
226+
internal/tui/page/chat.go−7
@@ -43,13 +43,6 @@ func (p *chatPage) Init() tea.Cmd {
4343 cmds := []tea.Cmd{
4444 p.layout.Init(),
4545 }
46−
47− sessions, _ := p.app.Sessions.List(context.Background())
48− if len(sessions) > 0 {
49− p.session = sessions[0]
50− cmd := p.setSidebar()
51− cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(p.session)), cmd)
52− }
5346 return tea.Batch(cmds...)
5447}
5548
internal/tui/tui.go+3−1
@@ -163,6 +163,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
163163 a.showPermissions = true
164164 return a, a.permissions.SetPermissions(msg.Payload)
165165 case dialog.PermissionResponseMsg:
166+ var cmd tea.Cmd
166167 switch msg.Action {
167168 case dialog.PermissionAllow:
168169 a.app.Permissions.Grant(msg.Permission)
@@ -170,9 +171,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
170171 a.app.Permissions.GrantPersistant(msg.Permission)
171172 case dialog.PermissionDeny:
172173 a.app.Permissions.Deny(msg.Permission)
174+ cmd = util.CmdHandler(chat.FocusEditorMsg(true))
173175 }
174176 a.showPermissions = false
175− return a, nil
177+ return a, cmd
176178
177179 case page.PageChangeMsg:
178180 return a, a.moveToPage(msg.ID)