diff options
| author | Kujtim Hoxha <[email protected]> | 2025-04-19 16:35:45 +0200 |
|---|---|---|
| committer | Kujtim Hoxha <[email protected]> | 2025-04-21 13:42:29 +0200 |
| commit | 2de51274177432b559be3b7deb1f14b9539f2994 (patch) | |
| tree | d47fff06ebf662f6bd7f594c78d0ae97e203a493 /internal/tui/components | |
| parent | 2b5a33e476ae3c6b5c6345777d20792786836dda (diff) | |
| download | opencode-2de51274177432b559be3b7deb1f14b9539f2994.tar.gz opencode-2de51274177432b559be3b7deb1f14b9539f2994.zip | |
initial tool call stream
Diffstat (limited to 'internal/tui/components')
| -rw-r--r-- | internal/tui/components/chat/list.go | 117 | ||||
| -rw-r--r-- | internal/tui/components/chat/message.go | 92 |
2 files changed, 101 insertions, 108 deletions
diff --git a/internal/tui/components/chat/list.go b/internal/tui/components/chat/list.go index 994ddea03..b09cc4495 100644 --- a/internal/tui/components/chat/list.go +++ b/internal/tui/components/chat/list.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "math" - "sync" - "time" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/spinner" @@ -13,7 +11,6 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/kujtimiihoxha/opencode/internal/app" - "github.com/kujtimiihoxha/opencode/internal/logging" "github.com/kujtimiihoxha/opencode/internal/message" "github.com/kujtimiihoxha/opencode/internal/pubsub" "github.com/kujtimiihoxha/opencode/internal/session" @@ -35,89 +32,14 @@ type messagesCmp struct { messages []message.Message uiMessages []uiMessage currentMsgID string - mutex sync.Mutex cachedContent map[string]cacheItem spinner spinner.Model - lastUpdate time.Time rendering bool } type renderFinishedMsg struct{} func (m *messagesCmp) Init() tea.Cmd { - return tea.Batch(m.viewport.Init()) -} - -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)() - } - if len(sessions) == 0 { - return nil - } - if len(sessions) > 20 { - sessions = sessions[:20] - } - for _, s := range sessions { - messages, err := m.app.Messages.List(context.Background(), s.ID) - if err != nil { - return util.ReportError(err)() - } - if len(messages) == 0 { - continue - } - m.cacheSessionMessages(messages, m.width) - - } - logging.Debug("preloaded sessions") - - return func() tea.Msg { - return renderFinishedMsg{} - } - } -} - -func (m *messagesCmp) cacheSessionMessages(messages []message.Message, width int) { - pos := 0 - if m.width == 0 { - return - } - for inx, msg := range messages { - switch msg.Role { - case message.User: - userMsg := renderUserMessage( - msg, - false, - width, - pos, - ) - m.cachedContent[msg.ID] = cacheItem{ - width: width, - content: []uiMessage{userMsg}, - } - pos += userMsg.height + 1 // + 1 for spacing - case message.Assistant: - assistantMessages := renderAssistantMessage( - msg, - inx, - messages, - m.app.Messages, - "", - width, - pos, - ) - for _, msg := range assistantMessages { - pos += msg.height + 1 // + 1 for spacing - } - m.cachedContent[msg.ID] = cacheItem{ - width: width, - content: assistantMessages, - } - } - } + return tea.Batch(m.viewport.Init(), m.spinner.Tick) } func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -360,21 +282,35 @@ func hasToolsWithoutResponse(messages []message.Message) bool { break } } - if !found { + if !found && v.Finished { return true } } + return false +} +func hasUnfinishedToolCalls(messages []message.Message) bool { + toolCalls := make([]message.ToolCall, 0) + for _, m := range messages { + toolCalls = append(toolCalls, m.ToolCalls()...) + } + for _, v := range toolCalls { + if !v.Finished { + return true + } + } return false } func (m *messagesCmp) working() string { text := "" - if m.IsAgentWorking() { + if m.IsAgentWorking() && len(m.messages) > 0 { task := "Thinking..." lastMessage := m.messages[len(m.messages)-1] if hasToolsWithoutResponse(m.messages) { task = "Waiting for tool response..." + } else if hasUnfinishedToolCalls(m.messages) { + task = "Building tool call..." } else if !lastMessage.IsFinished() { task = "Generating..." } @@ -434,8 +370,7 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd { delete(m.cachedContent, msg.ID) } m.uiMessages = make([]uiMessage, 0) - m.renderView() - return m.preloadSessions() + return nil } func (m *messagesCmp) GetSize() (int, int) { @@ -446,16 +381,16 @@ func (m *messagesCmp) SetSession(session session.Session) tea.Cmd { if m.session.ID == session.ID { return nil } + m.session = session + messages, err := m.app.Messages.List(context.Background(), session.ID) + if err != nil { + return util.ReportError(err) + } + m.messages = messages + m.currentMsgID = m.messages[len(m.messages)-1].ID + delete(m.cachedContent, m.currentMsgID) m.rendering = true return func() tea.Msg { - m.session = session - messages, err := m.app.Messages.List(context.Background(), session.ID) - if err != nil { - return util.ReportError(err) - } - m.messages = messages - m.currentMsgID = m.messages[len(m.messages)-1].ID - delete(m.cachedContent, m.currentMsgID) m.renderView() return renderFinishedMsg{} } diff --git a/internal/tui/components/chat/message.go b/internal/tui/components/chat/message.go index 14b9e268e..b8e450079 100644 --- a/internal/tui/components/chat/message.go +++ b/internal/tui/components/chat/message.go @@ -113,18 +113,10 @@ func renderAssistantMessage( width int, position int, ) []uiMessage { - // find the user message that is before this assistant message - var userMsg message.Message - for i := msgIndex - 1; i >= 0; i-- { - msg := allMessages[i] - if msg.Role == message.User { - userMsg = allMessages[i] - break - } - } - messages := []uiMessage{} content := msg.Content().String() + thinking := msg.IsThinking() + thinkingContent := msg.ReasoningContent().Thinking finished := msg.IsFinished() finishData := msg.FinishPart() info := []string{} @@ -133,7 +125,7 @@ func renderAssistantMessage( if finished { switch finishData.Reason { case message.FinishReasonEndTurn: - took := formatTimeDifference(userMsg.CreatedAt, finishData.Time) + took := formatTimeDifference(msg.CreatedAt, finishData.Time) info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render( fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took), )) @@ -166,6 +158,9 @@ func renderAssistantMessage( }) position += messages[0].height position++ // for the space + } else if thinking && thinkingContent != "" { + // Render the thinking content + content = renderMessage(thinkingContent, false, msg.ID == focusedUIMessageId, width) } for i, toolCall := range msg.ToolCalls() { @@ -218,10 +213,40 @@ func toolName(name string) string { return "View" case tools.WriteToolName: return "Write" + case tools.PatchToolName: + return "Patch" } return name } +func getToolAction(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..." + } + return "Working..." +} + // renders params, params[0] (params[1]=params[2] ....) func renderParams(paramsWidth int, params ...string) string { if len(params) == 0 { @@ -490,8 +515,47 @@ func renderToolMessage( if nested { width = width - 3 } + style := styles.BaseStyle. + Width(width - 1). + BorderLeft(true). + BorderStyle(lipgloss.ThickBorder()). + PaddingLeft(1). + BorderForeground(styles.ForgroundDim) + response := findToolResponse(toolCall.ID, allMessages) toolName := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%s: ", toolName(toolCall.Name))) + + if !toolCall.Finished { + // Get a brief description of what the tool is doing + toolAction := getToolAction(toolCall.Name) + + // toolInput := strings.ReplaceAll(toolCall.Input, "\n", " ") + // truncatedInput := toolInput + // if len(truncatedInput) > 10 { + // truncatedInput = truncatedInput[len(truncatedInput)-10:] + // } + // + // truncatedInput = styles.BaseStyle. + // Italic(true). + // Width(width - 2 - lipgloss.Width(toolName)). + // Background(styles.BackgroundDim). + // Foreground(styles.ForgroundMid). + // Render(truncatedInput) + + progressText := styles.BaseStyle. + Width(width - 2 - lipgloss.Width(toolName)). + Foreground(styles.ForgroundDim). + Render(fmt.Sprintf("%s", toolAction)) + + content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolName, progressText)) + toolMsg := uiMessage{ + messageType: toolMessageType, + position: position, + height: lipgloss.Height(content), + content: content, + } + return toolMsg + } params := renderToolParams(width-2-lipgloss.Width(toolName), toolCall) responseContent := "" if response != nil { @@ -504,12 +568,6 @@ func renderToolMessage( Foreground(styles.ForgroundDim). Render("Waiting for response...") } - style := styles.BaseStyle. - Width(width - 1). - BorderLeft(true). - BorderStyle(lipgloss.ThickBorder()). - PaddingLeft(1). - BorderForeground(styles.ForgroundDim) parts := []string{} if !nested { |
