diff --git a/.editorconfig b/.editorconfig index 5de2df8c5766460b43091d29af07636a58406434..0407ebbcbf7483988728000ce19928a0ffc3cdf6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,7 +11,7 @@ indent_size = 2 [*.go] indent_style = tab -indent_size = 8 +indent_size = 4 [*.golden] insert_final_newline = false diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index 4f31fe75d688aa2c4fdd80a4f633fe35d45125cc..511cf62996bd6e0d506a428344f34d89e515c82a 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -443,18 +443,14 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg assistantMsg.AppendContent(event.Content) return a.messages.Update(ctx, *assistantMsg) case provider.EventToolUseStart: + logging.Info("Tool call started", "toolCall", event.ToolCall) assistantMsg.AddToolCall(*event.ToolCall) return a.messages.Update(ctx, *assistantMsg) - // TODO: see how to handle this - // case provider.EventToolUseDelta: - // tm := time.Unix(assistantMsg.UpdatedAt, 0) - // assistantMsg.AppendToolCallInput(event.ToolCall.ID, event.ToolCall.Input) - // if time.Since(tm) > 1000*time.Millisecond { - // err := a.messages.Update(ctx, *assistantMsg) - // assistantMsg.UpdatedAt = time.Now().Unix() - // return err - // } + case provider.EventToolUseDelta: + assistantMsg.AppendToolCallInput(event.ToolCall.ID, event.ToolCall.Input) + return a.messages.Update(ctx, *assistantMsg) case provider.EventToolUseStop: + logging.Info("Finished tool call", "toolCall", event.ToolCall) assistantMsg.FinishToolCall(event.ToolCall.ID) return a.messages.Update(ctx, *assistantMsg) case provider.EventError: diff --git a/internal/llm/prompt/coder.go b/internal/llm/prompt/coder.go index 4cfa1314e0faab0d914e49949cbac01d80f8389c..495f2406a435fec54cfea9ac4abffd4e839c28e8 100644 --- a/internal/llm/prompt/coder.go +++ b/internal/llm/prompt/coder.go @@ -153,7 +153,7 @@ When making changes to files, first understand the file's code conventions. Mimi # Doing tasks The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended: -1. Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially. +1. Use the available search tools to understand the codebase and the user's query. 2. Implement the solution using all tools available to you 3. Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach. 4. VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to opencode.md so that you will know to run it next time. @@ -162,7 +162,7 @@ NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTAN # Tool usage policy - When doing file search, prefer to use the Agent tool in order to reduce context usage. -- If you intend to call multiple tools and there are no dependencies between the calls, make all of the independent calls in the same function_calls block. +- If you intend to call multiple tools and there are no dependencies between the calls, make all of the independent calls in parallel. - IMPORTANT: The user does not see the full output of the tool responses, so if you need the output of the tool for the response make sure to summarize it for the user. You MUST answer concisely with fewer than 4 lines of text (not including tool use or code generation), unless user asks for detail.` diff --git a/internal/llm/provider/anthropic.go b/internal/llm/provider/anthropic.go index badf6a3a07df27f6494bdbf9692f174e0a17a1ce..4b558e2fb18fe411e1dfbbc3652a2246375a9929 100644 --- a/internal/llm/provider/anthropic.go +++ b/internal/llm/provider/anthropic.go @@ -305,7 +305,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message ToolCall: &message.ToolCall{ ID: currentToolCallID, Finished: false, - Input: event.Delta.JSON.PartialJSON.Raw(), + Input: event.Delta.PartialJSON, }, } } diff --git a/internal/tui/components/anim/anim.go b/internal/tui/components/anim/anim.go index aed03d946d97a7e59a8fb4d08ba8a0c2bd30ffad..91ae8317eafaa6c49fce54194b8f1013d88042f4 100644 --- a/internal/tui/components/anim/anim.go +++ b/internal/tui/components/anim/anim.go @@ -9,6 +9,7 @@ import ( "github.com/charmbracelet/bubbles/v2/spinner" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" + "github.com/google/uuid" "github.com/lucasb-eyer/go-colorful" "github.com/opencode-ai/opencode/internal/tui/styles" "github.com/opencode-ai/opencode/internal/tui/theme" @@ -54,19 +55,23 @@ func (c cyclingChar) state(start time.Time) charState { return charCyclingState } -type StepCharsMsg struct{} +type StepCharsMsg struct { + id string +} -func stepChars() tea.Cmd { +func stepChars(id string) tea.Cmd { return tea.Tick(charCyclingFPS, func(time.Time) tea.Msg { - return StepCharsMsg{} + return StepCharsMsg{id} }) } -type ColorCycleMsg struct{} +type ColorCycleMsg struct { + id string +} -func cycleColors() tea.Cmd { +func cycleColors(id string) tea.Cmd { return tea.Tick(colorCycleFPS, func(time.Time) tea.Msg { - return ColorCycleMsg{} + return ColorCycleMsg{id} }) } @@ -80,6 +85,7 @@ type anim struct { label []rune ellipsis spinner.Model ellipsisStarted bool + id string } func New(cyclingCharsSize uint, label string) util.Model { @@ -91,10 +97,12 @@ func New(cyclingCharsSize uint, label string) util.Model { gap = "" } + id := uuid.New() c := anim{ start: time.Now(), label: []rune(gap + label), ellipsis: spinner.New(spinner.WithSpinner(spinner.Ellipsis)), + id: id.String(), } // If we're in truecolor mode (and there are enough cycling characters) @@ -144,15 +152,18 @@ func New(cyclingCharsSize uint, label string) util.Model { } // Init initializes the animation. -func (anim) Init() tea.Cmd { - return tea.Batch(stepChars(), cycleColors()) +func (a anim) Init() tea.Cmd { + return tea.Batch(stepChars(a.id), cycleColors(a.id)) } // Update handles messages. func (a anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd - switch msg.(type) { + switch msg := msg.(type) { case StepCharsMsg: + if msg.id != a.id { + return a, nil + } a.updateChars(&a.cyclingChars) a.updateChars(&a.labelChars) @@ -173,14 +184,17 @@ func (a anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - return a, tea.Batch(stepChars(), cmd) + return a, tea.Batch(stepChars(a.id), cmd) case ColorCycleMsg: + if msg.id != a.id { + return a, nil + } const minColorCycleSize = 2 if len(a.ramp) < minColorCycleSize { return a, nil } a.ramp = append(a.ramp[1:], a.ramp[0]) - return a, cycleColors() + return a, cycleColors(a.id) case spinner.TickMsg: var cmd tea.Cmd a.ellipsis, cmd = a.ellipsis.Update(msg) @@ -216,7 +230,7 @@ func (a anim) View() string { b.WriteRune(c.currentValue) } - if len(a.label) > 1 { + if len(a.labelChars) > 1 { textStyle := styles.BaseStyle(). Foreground(t.Text()) for _, c := range a.labelChars { diff --git a/internal/tui/components/chat/list_v2.go b/internal/tui/components/chat/list_v2.go index cc3e33db8a814e382d94fa6b2db4bcc9bb935cc8..52efd9b0b818a45ec1c045f0024cb35633a192e5 100644 --- a/internal/tui/components/chat/list_v2.go +++ b/internal/tui/components/chat/list_v2.go @@ -7,6 +7,7 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" "github.com/opencode-ai/opencode/internal/app" + "github.com/opencode-ai/opencode/internal/logging" "github.com/opencode-ai/opencode/internal/message" "github.com/opencode-ai/opencode/internal/pubsub" "github.com/opencode-ai/opencode/internal/session" @@ -61,7 +62,8 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.listCmp.SetItems([]util.Model{}) case pubsub.Event[message.Message]: - return m, m.handleMessageEvent(msg) + cmd := m.handleMessageEvent(msg) + return m, cmd default: var cmds []tea.Cmd u, cmd := m.listCmp.Update(msg) @@ -92,8 +94,8 @@ func (m *messageListCmp) handleMessageEvent(event pubsub.Event[message.Message]) // more likely to be at the end of the list items := m.listCmp.Items() for i := len(items) - 1; i >= 0; i-- { - msg := items[i].(messages.MessageCmp) - if msg.GetMessage().ID == event.Payload.ID { + msg, ok := items[i].(messages.MessageCmp) + if ok && msg.GetMessage().ID == event.Payload.ID { messageExists = true break } @@ -109,7 +111,6 @@ func (m *messageListCmp) handleMessageEvent(event pubsub.Event[message.Message]) case message.Tool: return m.handleToolMessage(event.Payload) } - // TODO: handle tools case pubsub.UpdatedEvent: return m.handleUpdateAssistantMessage(event.Payload) } @@ -122,30 +123,79 @@ func (m *messageListCmp) handleNewUserMessage(msg message.Message) tea.Cmd { } func (m *messageListCmp) handleToolMessage(msg message.Message) tea.Cmd { + items := m.listCmp.Items() + for _, tr := range msg.ToolResults() { + for i := len(items) - 1; i >= 0; i-- { + message := items[i] + if toolCall, ok := message.(messages.ToolCallCmp); ok { + if toolCall.GetToolCall().ID == tr.ToolCallID { + toolCall.SetToolResult(tr) + m.listCmp.UpdateItem( + i, + toolCall, + ) + break + } + } + } + } return nil } func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.Cmd { + var cmds []tea.Cmd // Simple update the content items := m.listCmp.Items() - lastItem := items[len(items)-1].(messages.MessageCmp) - // TODO:handle tool calls - if lastItem.GetMessage().ID != msg.ID { - return nil + assistantMessageInx := -1 + toolCalls := map[int]messages.ToolCallCmp{} + + // we go backwards because the messages are most likely at the end of the list + for i := len(items) - 1; i >= 0; i-- { + message := items[i] + if asMsg, ok := message.(messages.MessageCmp); ok { + if asMsg.GetMessage().ID == msg.ID { + assistantMessageInx = i + } + } else if tc, ok := message.(messages.ToolCallCmp); ok { + if tc.ParentMessageId() == msg.ID { + toolCalls[i] = tc + } + } } - // for now just updet the last message - if len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.IsThinking() { + + logging.Info("Update Assistant Message", "msg", msg, "assistantMessageInx", assistantMessageInx, "toolCalls", toolCalls) + + if assistantMessageInx > -1 && (len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.IsThinking()) { m.listCmp.UpdateItem( - len(items)-1, + assistantMessageInx, messages.NewMessageCmp( msg, messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)), ), ) - } else if len(msg.ToolCalls()) > 0 && msg.Content().Text == "" { - m.listCmp.DeleteItem(len(items) - 1) + } else if assistantMessageInx > -1 && len(msg.ToolCalls()) > 0 && msg.Content().Text == "" { + m.listCmp.DeleteItem(assistantMessageInx) } - return nil + for _, tc := range msg.ToolCalls() { + found := false + for inx, tcc := range toolCalls { + if tc.ID == tcc.GetToolCall().ID { + tcc.SetToolCall(tc) + m.listCmp.UpdateItem( + inx, + tcc, + ) + found = true + break + } + } + if !found { + cmd := m.listCmp.AppendItem(messages.NewToolCallCmp(msg.ID, tc)) + cmds = append(cmds, cmd) + } + } + + return tea.Batch(cmds...) } func (m *messageListCmp) handleNewAssistantMessage(msg message.Message) tea.Cmd { @@ -161,7 +211,7 @@ func (m *messageListCmp) handleNewAssistantMessage(msg message.Message) tea.Cmd cmds = append(cmds, cmd) } for _, tc := range msg.ToolCalls() { - cmd := m.listCmp.AppendItem(messages.NewToolCallCmp(tc)) + cmd := m.listCmp.AppendItem(messages.NewToolCallCmp(msg.ID, tc)) cmds = append(cmds, cmd) } return tea.Batch(cmds...) @@ -206,10 +256,10 @@ func (m *messageListCmp) SetSession(session session.Session) tea.Cmd { if tr, ok := toolResultMap[tc.ID]; ok { options = append(options, messages.WithToolCallResult(tr)) } - if msg.FinishPart().Reason == message.FinishReasonCanceled { + if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonCanceled { options = append(options, messages.WithToolCallCancelled()) } - uiMessages = append(uiMessages, messages.NewToolCallCmp(tc, options...)) + uiMessages = append(uiMessages, messages.NewToolCallCmp(msg.ID, tc, options...)) } } } diff --git a/internal/tui/components/chat/messages/messages.go b/internal/tui/components/chat/messages/messages.go index 3ae278a496df5ee60472172bc761bd07e43d8662..ede75252a0723674c3af6d98774438cd20fbf80f 100644 --- a/internal/tui/components/chat/messages/messages.go +++ b/internal/tui/components/chat/messages/messages.go @@ -65,9 +65,16 @@ func (m *messageCmp) Init() tea.Cmd { } func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - u, cmd := m.anim.Update(msg) - m.anim = u.(util.Model) - return m, cmd + switch msg := msg.(type) { + case anim.ColorCycleMsg, anim.StepCharsMsg: + m.spinning = m.shouldSpin() + if m.spinning { + u, cmd := m.anim.Update(msg) + m.anim = u.(util.Model) + return m, cmd + } + } + return m, nil } func (m *messageCmp) View() string { diff --git a/internal/tui/components/chat/messages/renderer.go b/internal/tui/components/chat/messages/renderer.go index ebea3eff09461fa87a44c0789e39dff232e023d7..28360830538ae717d0cb14761e04b72407fddc1a 100644 --- a/internal/tui/components/chat/messages/renderer.go +++ b/internal/tui/components/chat/messages/renderer.go @@ -524,32 +524,3 @@ func prettifyToolName(name string) string { return name } } - -func toolAction(name string) string { - switch name { - case agent.AgentToolName: - return "Preparing prompt..." - case tools.BashToolName: - return "Building command..." - case tools.EditToolName: - return "Preparing edit..." - case tools.FetchToolName: - return "Writing fetch..." - case tools.GlobToolName: - return "Finding files..." - case tools.GrepToolName: - return "Searching content..." - case tools.LSToolName: - return "Listing directory..." - case tools.SourcegraphToolName: - return "Searching code..." - case tools.ViewToolName: - return "Reading file..." - case tools.WriteToolName: - return "Preparing write..." - case tools.PatchToolName: - return "Preparing patch..." - default: - return "Working..." - } -} diff --git a/internal/tui/components/chat/messages/tool.go b/internal/tui/components/chat/messages/tool.go index 9bdca071a999a031e20fe568efde27e92862a82c..d51f659262b83ff6a7ec6cc70c3c4eda189fe987 100644 --- a/internal/tui/components/chat/messages/tool.go +++ b/internal/tui/components/chat/messages/tool.go @@ -6,9 +6,9 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/x/ansi" - "github.com/opencode-ai/opencode/internal/llm/agent" - "github.com/opencode-ai/opencode/internal/llm/tools" + "github.com/opencode-ai/opencode/internal/logging" "github.com/opencode-ai/opencode/internal/message" + "github.com/opencode-ai/opencode/internal/tui/components/anim" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" "github.com/opencode-ai/opencode/internal/tui/theme" @@ -21,15 +21,24 @@ type ToolCallCmp interface { layout.Focusable GetToolCall() message.ToolCall GetToolResult() message.ToolResult + SetToolResult(message.ToolResult) + SetToolCall(message.ToolCall) + SetCancelled() + ParentMessageId() string + Spinning() bool } type toolCallCmp struct { width int focused bool - call message.ToolCall - result message.ToolResult - cancelled bool + parentMessageId string + call message.ToolCall + result message.ToolResult + cancelled bool + + spinning bool + anim util.Model } type ToolCallOption func(*toolCallCmp) @@ -46,9 +55,11 @@ func WithToolCallResult(result message.ToolResult) ToolCallOption { } } -func NewToolCallCmp(tc message.ToolCall, opts ...ToolCallOption) ToolCallCmp { +func NewToolCallCmp(parentMessageId string, tc message.ToolCall, opts ...ToolCallOption) ToolCallCmp { m := &toolCallCmp{ - call: tc, + call: tc, + parentMessageId: parentMessageId, + anim: anim.New(15, "Working"), } for _, opt := range opts { opt(m) @@ -57,10 +68,24 @@ func NewToolCallCmp(tc message.ToolCall, opts ...ToolCallOption) ToolCallCmp { } func (m *toolCallCmp) Init() tea.Cmd { + m.spinning = m.shouldSpin() + logging.Info("Initializing tool call spinner", "tool_call", m.call.Name, "spinning", m.spinning) + if m.spinning { + return m.anim.Init() + } return nil } func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + logging.Debug("Tool call update", "msg", msg) + switch msg := msg.(type) { + case anim.ColorCycleMsg, anim.StepCharsMsg: + if m.spinning { + u, cmd := m.anim.Update(msg) + m.anim = u.(util.Model) + return m, cmd + } + } return m, nil } @@ -75,6 +100,30 @@ func (m *toolCallCmp) View() string { return box.PaddingLeft(1).Render(r.Render(m)) } +// SetCancelled implements ToolCallCmp. +func (m *toolCallCmp) SetCancelled() { + m.cancelled = true +} + +// SetToolCall implements ToolCallCmp. +func (m *toolCallCmp) SetToolCall(call message.ToolCall) { + m.call = call + if m.call.Finished { + m.spinning = false + } +} + +// ParentMessageId implements ToolCallCmp. +func (m *toolCallCmp) ParentMessageId() string { + return m.parentMessageId +} + +// SetToolResult implements ToolCallCmp. +func (m *toolCallCmp) SetToolResult(result message.ToolResult) { + m.result = result + m.spinning = false +} + // GetToolCall implements ToolCallCmp. func (m *toolCallCmp) GetToolCall() message.ToolCall { return m.call @@ -86,7 +135,7 @@ func (m *toolCallCmp) GetToolResult() message.ToolResult { } func (m *toolCallCmp) renderPending() string { - return fmt.Sprintf("%s: %s", prettifyToolName(m.call.Name), toolAction(m.call.Name)) + return fmt.Sprintf("%s: %s", prettifyToolName(m.call.Name), m.anim.View()) } func (m *toolCallCmp) style() lipgloss.Style { @@ -113,35 +162,6 @@ func (m *toolCallCmp) fit(content string, width int) string { return ansi.Truncate(content, width, dots) } -func (m *toolCallCmp) toolName() string { - switch m.call.Name { - case agent.AgentToolName: - return "Task" - case tools.BashToolName: - return "Bash" - case tools.EditToolName: - return "Edit" - case tools.FetchToolName: - return "Fetch" - case tools.GlobToolName: - return "Glob" - case tools.GrepToolName: - return "Grep" - case tools.LSToolName: - return "List" - case tools.SourcegraphToolName: - return "Sourcegraph" - case tools.ViewToolName: - return "View" - case tools.WriteToolName: - return "Write" - case tools.PatchToolName: - return "Patch" - default: - return m.call.Name - } -} - func (m *toolCallCmp) Blur() tea.Cmd { m.focused = false return nil @@ -165,3 +185,16 @@ func (m *toolCallCmp) SetSize(width int, height int) tea.Cmd { m.width = width return nil } + +func (m *toolCallCmp) shouldSpin() bool { + if !m.call.Finished { + return true + } else if m.result.ToolCallID != m.call.ID { + return true + } + return false +} + +func (m *toolCallCmp) Spinning() bool { + return m.spinning +} diff --git a/internal/tui/components/core/list/list.go b/internal/tui/components/core/list/list.go index 3827fcdd7be95ea230bb432f677f559289415119..628b6835f83ec17d0a853889043290c7244a5006 100644 --- a/internal/tui/components/core/list/list.go +++ b/internal/tui/components/core/list/list.go @@ -9,7 +9,6 @@ import ( "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" - "github.com/opencode-ai/opencode/internal/logging" "github.com/opencode-ai/opencode/internal/tui/components/anim" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/util" @@ -39,7 +38,7 @@ type renderedItem struct { } type model struct { width, height, offset int - finalHight int // this gets set when the last item is rendered to mark the max offset + finalHeight int // this gets set when the last item is rendered to mark the max offset reverse bool help help.Model keymap KeyMap @@ -95,7 +94,7 @@ func New(opts ...listOptions) ListModel { gapSize: 0, padding: []int{}, selectedItemInx: -1, - finalHight: -1, + finalHeight: -1, lastRenderedInx: -1, renderedItems: new(sync.Map), } @@ -160,20 +159,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.goToBottom() return m, nil } - case anim.ColorCycleMsg: - logging.Info("ColorCycleMsg", "msg", msg) - for inx, item := range m.items { - if i, ok := item.(HasAnim); ok { - if i.Spinning() { - updated, cmd := i.Update(msg) - cmds = append(cmds, cmd) - m.UpdateItem(inx, updated.(util.Model)) - } - } - } - return m, tea.Batch(cmds...) - case anim.StepCharsMsg: - logging.Info("ColorCycleMsg", "msg", msg) + case anim.ColorCycleMsg, anim.StepCharsMsg: for inx, item := range m.items { if i, ok := item.(HasAnim); ok { if i.Spinning() { @@ -189,7 +175,6 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { u, cmd := m.items[m.selectedItemInx].Update(msg) cmds = append(cmds, cmd) m.UpdateItem(m.selectedItemInx, u.(util.Model)) - cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } @@ -232,7 +217,7 @@ func (m *model) renderVisibleReverse() { itemLines = cachedContent.(renderedItem).lines } else { itemLines = strings.Split(items[i].View(), "\n") - if m.gapSize > 0 && realIndex != len(m.items)-1 { + if m.gapSize > 0 { for range m.gapSize { itemLines = append(itemLines, "") } @@ -245,7 +230,7 @@ func (m *model) renderVisibleReverse() { } if realIndex == 0 { - m.finalHight = max(0, start+len(itemLines)-m.listHeight()) + m.finalHeight = max(0, start+len(itemLines)-m.listHeight()) } m.renderedLines = append(itemLines, m.renderedLines...) m.lastRenderedInx = realIndex @@ -256,9 +241,9 @@ func (m *model) renderVisibleReverse() { start += len(itemLines) } m.needsRerender = false - if m.finalHight > -1 { + if m.finalHeight > -1 { // make sure we don't go over the final height, this can happen if we did not render the last item and we overshot the offset - m.offset = min(m.offset, m.finalHight) + m.offset = min(m.offset, m.finalHeight) } maxHeight := min(m.listHeight(), len(m.renderedLines)) if m.offset < len(m.renderedLines) { @@ -293,7 +278,7 @@ func (m *model) renderVisible() { itemLines = cachedContent.(renderedItem).lines } else { itemLines = strings.Split(item.View(), "\n") - if m.gapSize > 0 && realIndex != len(m.items)-1 { + if m.gapSize > 0 { for range m.gapSize { itemLines = append(itemLines, "") } @@ -310,7 +295,7 @@ func (m *model) renderVisible() { } if realIndex == len(m.items)-1 { - m.finalHight = max(0, start+len(itemLines)-m.listHeight()) + m.finalHeight = max(0, start+len(itemLines)-m.listHeight()) } m.renderedLines = append(m.renderedLines, itemLines...) @@ -319,9 +304,9 @@ func (m *model) renderVisible() { } m.needsRerender = false maxHeight := min(m.listHeight(), len(m.renderedLines)) - if m.finalHight > -1 { + if m.finalHeight > -1 { // make sure we don't go over the final height, this can happen if we did not render the last item and we overshot the offset - m.offset = min(m.offset, m.finalHight) + m.offset = min(m.offset, m.finalHeight) } if m.offset < len(m.renderedLines) { m.content = strings.Join(m.renderedLines[m.offset:maxHeight+m.offset], "\n") @@ -412,6 +397,9 @@ func (m *model) downOneItem() tea.Cmd { } func (m *model) goToBottom() tea.Cmd { + if len(m.items) == 0 { + return nil + } var cmds []tea.Cmd m.reverse = true cmd := m.blurSelected() @@ -428,11 +416,14 @@ func (m *model) ResetView() { m.renderedLines = []string{} m.offset = 0 m.lastRenderedInx = -1 - m.finalHight = -1 + m.finalHeight = -1 m.needsRerender = true } func (m *model) goToTop() tea.Cmd { + if len(m.items) == 0 { + return nil + } var cmds []tea.Cmd m.reverse = false cmd := m.blurSelected() @@ -480,7 +471,7 @@ func (m *model) rerenderItem(inx int) { } rerenderedItem := m.items[inx].View() rerenderedLines := strings.Split(rerenderedItem, "\n") - if m.gapSize > 0 && inx != len(m.items)-1 { + if m.gapSize > 0 { for range m.gapSize { rerenderedLines = append(rerenderedLines, "") } @@ -492,7 +483,6 @@ func (m *model) rerenderItem(inx int) { } // check if the item is in the content start := cachedItem.start - logging.Info("rerenderItem", "inx", inx, "start", start, "cachedItem.start", cachedItem.start, "cachedItem.height", cachedItem.height) end := start + cachedItem.height totalLines := len(m.renderedLines) if m.reverse { @@ -504,9 +494,35 @@ func (m *model) rerenderItem(inx int) { m.renderedLines = slices.Insert(m.renderedLines, start, rerenderedLines...) } // TODO: if hight changed do something - if cachedItem.height != len(rerenderedLines) && inx != len(m.items)-1 { + if cachedItem.height != len(rerenderedLines) { if inx == len(m.items)-1 { - m.finalHight = max(0, start+len(rerenderedLines)-m.listHeight()) + m.finalHeight = max(0, start+len(rerenderedLines)-m.listHeight()) + } + + // update the start of the other cached items + currentStart := cachedItem.start + len(rerenderedLines) + if m.reverse { + for i := inx - 1; i < len(m.items); i-- { + if existing, ok := m.renderedItems.Load(i); ok { + cached := existing.(renderedItem) + cached.start = currentStart + currentStart += cached.height + m.renderedItems.Store(i, cached) + } else { + break + } + } + } else { + for i := inx + 1; i < len(m.items); i++ { + if existing, ok := m.renderedItems.Load(i); ok { + cached := existing.(renderedItem) + cached.start = currentStart + currentStart += cached.height + m.renderedItems.Store(i, cached) + } else { + break + } + } } } m.renderedItems.Store(inx, renderedItem{ @@ -518,11 +534,11 @@ func (m *model) rerenderItem(inx int) { } func (m *model) increaseOffset(n int) { - if m.finalHight > -1 { - if m.offset < m.finalHight { + if m.finalHeight > -1 { + if m.offset < m.finalHeight { m.offset += n - if m.offset > m.finalHight { - m.offset = m.finalHight + if m.offset > m.finalHeight { + m.offset = m.finalHeight } m.needsRerender = true } @@ -550,7 +566,8 @@ func (m *model) UpdateItem(inx int, item util.Model) { i.Focus() } } - m.ResetView() + m.setItemSize(inx) + m.rerenderItem(inx) m.needsRerender = true } @@ -565,16 +582,15 @@ func (m *model) SetSize(width int, height int) tea.Cmd { return nil } if m.height != height { - m.finalHight = -1 + m.finalHeight = -1 m.height = height } m.width = width m.ResetView() - return m.setItemsSize() + return m.setAllItemsSize() } -func (m *model) setItemsSize() tea.Cmd { - var cmds []tea.Cmd +func (m *model) getItemSize() int { width := m.width if m.padding != nil { if len(m.padding) == 1 { @@ -585,11 +601,22 @@ func (m *model) setItemsSize() tea.Cmd { width -= m.padding[1] + m.padding[3] } } - for _, item := range m.items { - if i, ok := item.(layout.Sizeable); ok { - cmd := i.SetSize(width, 0) // height is not limited - cmds = append(cmds, cmd) - } + return width +} + +func (m *model) setItemSize(inx int) tea.Cmd { + if i, ok := m.items[inx].(layout.Sizeable); ok { + cmd := i.SetSize(m.getItemSize(), 0) // height is not limited + return cmd + } + return nil +} + +func (m *model) setAllItemsSize() tea.Cmd { + var cmds []tea.Cmd + for i := range m.items { + cmd := m.setItemSize(i) + cmds = append(cmds, cmd) } return tea.Batch(cmds...) } @@ -612,11 +639,16 @@ func (m *model) listHeight() int { // AppendItem implements List. func (m *model) AppendItem(item util.Model) tea.Cmd { + var cmds []tea.Cmd cmd := item.Init() + cmds = append(cmds, cmd) m.items = append(m.items, item) - m.goToBottom() + cmd = m.setItemSize(len(m.items) - 1) + cmds = append(cmds, cmd) + cmd = m.goToBottom() + cmds = append(cmds, cmd) m.needsRerender = true - return cmd + return tea.Batch(cmds...) } // DeleteItem implements List. @@ -632,7 +664,9 @@ func (m *model) DeleteItem(i int) { // PrependItem implements List. func (m *model) PrependItem(item util.Model) tea.Cmd { + var cmds []tea.Cmd cmd := item.Init() + cmds = append(cmds, cmd) m.items = append([]util.Model{item}, m.items...) // update the indices of the rendered items newRenderedItems := make(map[int]renderedItem) @@ -647,9 +681,12 @@ func (m *model) PrependItem(item util.Model) tea.Cmd { for k, v := range newRenderedItems { m.renderedItems.Store(k, v) } - m.goToTop() + cmd = m.goToTop() + cmds = append(cmds, cmd) + cmd = m.setItemSize(0) + cmds = append(cmds, cmd) m.needsRerender = true - return cmd + return tea.Batch(cmds...) } func (m *model) setReverse(reverse bool) { @@ -664,7 +701,7 @@ func (m *model) setReverse(reverse bool) { func (m *model) SetItems(items []util.Model) tea.Cmd { m.items = items var cmds []tea.Cmd - cmd := m.setItemsSize() + cmd := m.setAllItemsSize() cmds = append(cmds, cmd) for _, item := range m.items { cmds = append(cmds, item.Init()) @@ -678,7 +715,6 @@ func (m *model) SetItems(items []util.Model) tea.Cmd { cmd := m.focusSelected() cmds = append(cmds, cmd) } - m.needsRerender = true m.ResetView() return tea.Batch(cmds...) } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 94b8c958bfec4d546ccd642cea3ecc515d5c7931..75f90d97b6c4c8117fcd28bbb3f2cfdf96934777 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -186,7 +186,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.WindowSizeMsg: - logging.Info("Window size changed main: ", "Width", msg.Width, "Height", msg.Height) msg.Height -= 1 // Make space for the status bar a.width, a.height = msg.Width, msg.Height