.editorconfig 🔗
@@ -11,7 +11,7 @@ indent_size = 2
[*.go]
indent_style = tab
-indent_size = 8
+indent_size = 4
[*.golden]
insert_final_newline = false
Kujtim Hoxha created
.editorconfig | 2
internal/llm/agent/agent.go | 14 -
internal/llm/prompt/coder.go | 4
internal/llm/provider/anthropic.go | 2
internal/tui/components/anim/anim.go | 38 +++-
internal/tui/components/chat/list_v2.go | 84 ++++++++--
internal/tui/components/chat/messages/messages.go | 13 +
internal/tui/components/chat/messages/renderer.go | 29 ---
internal/tui/components/chat/messages/tool.go | 107 ++++++++----
internal/tui/components/core/list/list.go | 138 ++++++++++------
internal/tui/tui.go | 1
11 files changed, 269 insertions(+), 163 deletions(-)
@@ -11,7 +11,7 @@ indent_size = 2
[*.go]
indent_style = tab
-indent_size = 8
+indent_size = 4
[*.golden]
insert_final_newline = false
@@ -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:
@@ -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.`
@@ -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,
},
}
}
@@ -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 {
@@ -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...))
}
}
}
@@ -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 {
@@ -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..."
- }
-}
@@ -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
+}
@@ -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...)
}
@@ -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