wip: refactoring tui

This commit is contained in:
adamdottv
2025-05-29 09:42:56 -05:00
parent a9799136fe
commit 6759674c0f
12 changed files with 345 additions and 1524 deletions

View File

@@ -3,7 +3,6 @@ package app
import (
"context"
"fmt"
"maps"
"sync"
"time"
@@ -12,7 +11,6 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/sst/opencode/internal/config"
"github.com/sst/opencode/internal/fileutil"
"github.com/sst/opencode/internal/lsp"
"github.com/sst/opencode/internal/message"
"github.com/sst/opencode/internal/session"
"github.com/sst/opencode/internal/status"
@@ -23,23 +21,21 @@ import (
)
type App struct {
State map[string]any
Client *client.ClientWithResponses
Events *client.Client
State map[string]any
Session *client.SessionInfo
Messages []client.SessionMessage
CurrentSession *session.Session
Logs any // TODO: Define LogService interface when needed
Sessions SessionService
Messages MessageService
History any // TODO: Define HistoryService interface when needed
Permissions any // TODO: Define PermissionService interface when needed
Status status.Service
Client *client.ClientWithResponses
Events *client.Client
CurrentSessionOLD *session.Session
SessionsOLD SessionService
MessagesOLD MessageService
LogsOLD any // TODO: Define LogService interface when needed
HistoryOLD any // TODO: Define HistoryService interface when needed
PermissionsOLD any // TODO: Define PermissionService interface when needed
Status status.Service
PrimaryAgent AgentService
LSPClients map[string]*lsp.Client
clientsMutex sync.RWMutex
PrimaryAgentOLD AgentService
watcherCancelFuncs []context.CancelFunc
cancelFuncsMutex sync.Mutex
@@ -80,20 +76,20 @@ func New(ctx context.Context) (*App, error) {
agentBridge := NewAgentServiceBridge(httpClient)
app := &App{
State: make(map[string]any),
Client: httpClient,
Events: eventClient,
CurrentSession: &session.Session{},
Sessions: sessionBridge,
Messages: messageBridge,
PrimaryAgent: agentBridge,
Status: status.GetService(),
LSPClients: make(map[string]*lsp.Client),
State: make(map[string]any),
Client: httpClient,
Events: eventClient,
Session: &client.SessionInfo{},
CurrentSessionOLD: &session.Session{},
SessionsOLD: sessionBridge,
MessagesOLD: messageBridge,
PrimaryAgentOLD: agentBridge,
Status: status.GetService(),
// TODO: These services need API endpoints:
Logs: nil, // logging.GetService(),
History: nil, // history.GetService(),
Permissions: nil, // permission.GetService(),
LogsOLD: nil, // logging.GetService(),
HistoryOLD: nil, // history.GetService(),
PermissionsOLD: nil, // permission.GetService(),
}
// Initialize theme based on configuration
@@ -105,30 +101,28 @@ func New(ctx context.Context) (*App, error) {
// Create creates a new session
func (a *App) SendChatMessage(ctx context.Context, text string, attachments []message.Attachment) tea.Cmd {
var cmds []tea.Cmd
if a.CurrentSession.ID == "" {
if a.Session.Id == "" {
resp, err := a.Client.PostSessionCreateWithResponse(ctx)
if err != nil {
// return session.Session{}, err
status.Error(err.Error())
return nil
}
if resp.StatusCode() != 200 {
// return session.Session{}, fmt.Errorf("failed to create session: %d", resp.StatusCode())
status.Error(fmt.Sprintf("failed to create session: %d", resp.StatusCode()))
return nil
}
info := resp.JSON200
// Convert to old session type
info := resp.JSON200
a.Session = info
// Convert to old session type for backwards compatibility
newSession := session.Session{
ID: info.Id,
Title: info.Title,
CreatedAt: time.Now(), // API doesn't provide this yet
UpdatedAt: time.Now(), // API doesn't provide this yet
}
if err != nil {
status.Error(err.Error())
return nil
}
a.CurrentSession = &newSession
a.CurrentSessionOLD = &newSession
cmds = append(cmds, util.CmdHandler(state.SessionSelectedMsg(&newSession)))
}
@@ -147,7 +141,7 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []me
parts := []client.SessionMessagePart{part}
go a.Client.PostSessionChatWithResponse(ctx, client.PostSessionChatJSONRequestBody{
SessionID: a.CurrentSession.ID,
SessionID: a.Session.Id,
Parts: parts,
ProviderID: "anthropic",
ModelID: "claude-sonnet-4-20250514",
@@ -234,18 +228,4 @@ func (app *App) Shutdown() {
}
app.cancelFuncsMutex.Unlock()
app.watcherWG.Wait()
// Perform additional cleanup for LSP clients
app.clientsMutex.RLock()
clients := make(map[string]*lsp.Client, len(app.LSPClients))
maps.Copy(clients, app.LSPClients)
app.clientsMutex.RUnlock()
for name, client := range clients {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
if err := client.Shutdown(shutdownCtx); err != nil {
slog.Error("Failed to shutdown LSP client", "name", name, "error", err)
}
cancel()
}
}

View File

@@ -143,7 +143,7 @@ func (m *editorCmp) Init() tea.Cmd {
}
func (m *editorCmp) send() tea.Cmd {
if m.app.PrimaryAgent.IsSessionBusy(m.app.CurrentSession.ID) {
if m.app.PrimaryAgentOLD.IsSessionBusy(m.app.CurrentSessionOLD.ID) {
status.Warn("Agent is working, please wait...")
return nil
}
@@ -217,7 +217,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
if key.Matches(msg, editorMaps.OpenEditor) {
if m.app.PrimaryAgent.IsSessionBusy(m.app.CurrentSession.ID) {
if m.app.PrimaryAgentOLD.IsSessionBusy(m.app.CurrentSessionOLD.ID) {
status.Warn("Agent is working, please wait...")
return m, nil
}

View File

@@ -1,7 +1,6 @@
package chat
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
@@ -12,12 +11,12 @@ import (
"github.com/charmbracelet/x/ansi"
"github.com/sst/opencode/internal/config"
"github.com/sst/opencode/internal/diff"
"github.com/sst/opencode/internal/llm/agent"
"github.com/sst/opencode/internal/llm/models"
"github.com/sst/opencode/internal/llm/tools"
"github.com/sst/opencode/internal/message"
"github.com/sst/opencode/internal/tui/styles"
"github.com/sst/opencode/internal/tui/theme"
"github.com/sst/opencode/pkg/client"
)
type uiMessageType int
@@ -33,8 +32,6 @@ const (
type uiMessage struct {
ID string
messageType uiMessageType
position int
height int
content string
}
@@ -48,7 +45,7 @@ func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...s
t := theme.CurrentTheme()
style := styles.BaseStyle().
Width(width - 1).
// Width(width - 1).
BorderLeft(true).
Foreground(t.TextMuted()).
BorderForeground(t.Primary()).
@@ -79,28 +76,29 @@ func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...s
return rendered
}
func renderUserMessage(msg message.Message, isFocused bool, width int, position int) uiMessage {
var styledAttachments []string
func renderUserMessage(msg client.SessionMessage, isFocused bool, width int, position int) uiMessage {
// var styledAttachments []string
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
attachmentStyles := baseStyle.
MarginLeft(1).
Background(t.TextMuted()).
Foreground(t.Text())
for _, attachment := range msg.BinaryContent() {
file := filepath.Base(attachment.Path)
var filename string
if len(file) > 10 {
filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, file[0:7])
} else {
filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, file)
}
styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
}
// attachmentStyles := baseStyle.
// MarginLeft(1).
// Background(t.TextMuted()).
// Foreground(t.Text())
// for _, attachment := range msg.BinaryContent() {
// file := filepath.Base(attachment.Path)
// var filename string
// if len(file) > 10 {
// filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, file[0:7])
// } else {
// filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, file)
// }
// styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
// }
info := []string{}
// Add timestamp info
info := []string{}
timestamp := msg.CreatedAt.Local().Format("02 Jan 2006 03:04 PM")
timestamp := time.UnixMilli(int64(msg.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
username, _ := config.GetUsername()
info = append(info, baseStyle.
Width(width-1).
@@ -109,17 +107,27 @@ func renderUserMessage(msg message.Message, isFocused bool, width int, position
)
content := ""
if len(styledAttachments) > 0 {
attachmentContent := baseStyle.Width(width).Render(lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...))
content = renderMessage(msg.Content().String(), true, isFocused, width, append(info, attachmentContent)...)
} else {
content = renderMessage(msg.Content().String(), true, isFocused, width, info...)
// if len(styledAttachments) > 0 {
// attachmentContent := baseStyle.Width(width).Render(lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...))
// content = renderMessage(msg.Content().String(), true, isFocused, width, append(info, attachmentContent)...)
// } else {
for _, p := range msg.Parts {
part, err := p.ValueByDiscriminator()
if err != nil {
continue //TODO: handle error?
}
switch part.(type) {
case client.SessionMessagePartText:
textPart := part.(client.SessionMessagePartText)
content = renderMessage(textPart.Text, true, isFocused, width, info...)
}
}
// content = renderMessage(msg.Parts, true, isFocused, width, info...)
userMsg := uiMessage{
ID: msg.ID,
ID: msg.Id,
messageType: userMessageType,
position: position,
height: lipgloss.Height(content),
content: content,
}
return userMsg
@@ -193,11 +201,11 @@ func renderAssistantMessage(
messages = append(messages, uiMessage{
ID: msg.ID,
messageType: assistantMessageType,
position: position,
height: lipgloss.Height(content),
content: content,
// position: position,
// height: lipgloss.Height(content),
content: content,
})
position += messages[0].height
// position += messages[0].height
position++ // for the space
} else if thinking && thinkingContent != "" {
// Render the thinking content with timestamp
@@ -205,9 +213,9 @@ func renderAssistantMessage(
messages = append(messages, uiMessage{
ID: msg.ID,
messageType: assistantMessageType,
position: position,
height: lipgloss.Height(content),
content: content,
// position: position,
// height: lipgloss.Height(content),
content: content,
})
position += lipgloss.Height(content)
position++ // for the space
@@ -226,7 +234,7 @@ func renderAssistantMessage(
i+1,
)
messages = append(messages, toolCallContent)
position += toolCallContent.height
// position += toolCallContent.height
position++ // for the space
}
}
@@ -246,8 +254,8 @@ func findToolResponse(toolCallID string, futureMessages []message.Message) *mess
func toolName(name string) string {
switch name {
case agent.AgentToolName:
return "Task"
// case agent.AgentToolName:
// return "Task"
case tools.BashToolName:
return "Bash"
case tools.EditToolName:
@@ -274,8 +282,8 @@ func toolName(name string) string {
func getToolAction(name string) string {
switch name {
case agent.AgentToolName:
return "Preparing prompt..."
// case agent.AgentToolName:
// return "Preparing prompt..."
case tools.BashToolName:
return "Building command..."
case tools.EditToolName:
@@ -363,11 +371,11 @@ func removeWorkingDirPrefix(path string) string {
func renderToolParams(paramWidth int, toolCall message.ToolCall) string {
params := ""
switch toolCall.Name {
case agent.AgentToolName:
var params agent.AgentParams
json.Unmarshal([]byte(toolCall.Input), &params)
prompt := strings.ReplaceAll(params.Prompt, "\n", " ")
return renderParams(paramWidth, prompt)
// case agent.AgentToolName:
// var params agent.AgentParams
// json.Unmarshal([]byte(toolCall.Input), &params)
// prompt := strings.ReplaceAll(params.Prompt, "\n", " ")
// return renderParams(paramWidth, prompt)
case tools.BashToolName:
var params tools.BashParams
json.Unmarshal([]byte(toolCall.Input), &params)
@@ -481,11 +489,11 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
resultContent := truncateHeight(response.Content, maxResultHeight)
switch toolCall.Name {
case agent.AgentToolName:
return styles.ForceReplaceBackgroundWithLipgloss(
toMarkdown(resultContent, false, width),
t.Background(),
)
// case agent.AgentToolName:
// return styles.ForceReplaceBackgroundWithLipgloss(
// toMarkdown(resultContent, false, width),
// t.Background(),
// )
case tools.BashToolName:
resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent)
return styles.ForceReplaceBackgroundWithLipgloss(
@@ -628,9 +636,9 @@ func renderToolMessage(
content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText))
toolMsg := uiMessage{
messageType: toolMessageType,
position: position,
height: lipgloss.Height(content),
content: content,
// position: position,
// height: lipgloss.Height(content),
content: content,
}
return toolMsg
}
@@ -667,17 +675,17 @@ func renderToolMessage(
parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolNameText, formattedParams))
}
if toolCall.Name == agent.AgentToolName {
taskMessages, _ := messagesService.List(context.Background(), toolCall.ID)
toolCalls := []message.ToolCall{}
for _, v := range taskMessages {
toolCalls = append(toolCalls, v.ToolCalls()...)
}
for _, call := range toolCalls {
rendered := renderToolMessage(call, []message.Message{}, messagesService, focusedUIMessageId, true, width, 0)
parts = append(parts, rendered.content)
}
}
// if toolCall.Name == agent.AgentToolName {
// taskMessages, _ := messagesService.List(context.Background(), toolCall.ID)
// toolCalls := []message.ToolCall{}
// for _, v := range taskMessages {
// toolCalls = append(toolCalls, v.ToolCalls()...)
// }
// for _, call := range toolCalls {
// rendered := renderToolMessage(call, []message.Message{}, messagesService, focusedUIMessageId, true, width, 0)
// parts = append(parts, rendered.content)
// }
// }
if responseContent != "" && !nested {
parts = append(parts, responseContent)
}
@@ -696,9 +704,9 @@ func renderToolMessage(
}
toolMsg := uiMessage{
messageType: toolMessageType,
position: position,
height: lipgloss.Height(content),
content: content,
// position: position,
// height: lipgloss.Height(content),
content: content,
}
return toolMsg
}

View File

@@ -1,8 +1,6 @@
package chat
import (
"context"
"encoding/json"
"fmt"
"math"
"time"
@@ -13,14 +11,13 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/sst/opencode/internal/message"
"github.com/sst/opencode/internal/pubsub"
"github.com/sst/opencode/internal/session"
"github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/tui/app"
"github.com/sst/opencode/internal/tui/components/dialog"
"github.com/sst/opencode/internal/tui/state"
"github.com/sst/opencode/internal/tui/styles"
"github.com/sst/opencode/internal/tui/theme"
"github.com/sst/opencode/pkg/client"
)
type cacheItem struct {
@@ -32,7 +29,6 @@ type messagesCmp struct {
app *app.App
width, height int
viewport viewport.Model
messages []message.Message
uiMessages []uiMessage
currentMsgID string
cachedContent map[string]cacheItem
@@ -75,6 +71,8 @@ func (m *messagesCmp) Init() tea.Cmd {
}
func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.renderView()
var cmds []tea.Cmd
switch msg := msg.(type) {
case dialog.ThemeChangedMsg:
@@ -90,7 +88,7 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmd := m.Reload(msg)
return m, cmd
case state.SessionClearedMsg:
m.messages = make([]message.Message, 0)
// m.messages = make([]message.Message, 0)
m.currentMsgID = ""
m.rendering = false
return m, nil
@@ -104,62 +102,63 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case renderFinishedMsg:
m.rendering = false
m.viewport.GotoBottom()
case state.StateUpdatedMsg:
m.renderView()
m.viewport.GotoBottom()
case pubsub.Event[message.Message]:
needsRerender := false
if msg.Type == message.EventMessageCreated {
if msg.Payload.SessionID == m.app.CurrentSession.ID {
messageExists := false
for _, v := range m.messages {
if v.ID == msg.Payload.ID {
messageExists = true
break
}
}
if !messageExists {
if len(m.messages) > 0 {
lastMsgID := m.messages[len(m.messages)-1].ID
delete(m.cachedContent, lastMsgID)
}
m.messages = append(m.messages, msg.Payload)
delete(m.cachedContent, m.currentMsgID)
m.currentMsgID = msg.Payload.ID
needsRerender = true
}
}
// There are tool calls from the child task
for _, v := range m.messages {
for _, c := range v.ToolCalls() {
if c.ID == msg.Payload.SessionID {
delete(m.cachedContent, v.ID)
needsRerender = true
}
}
}
} else if msg.Type == message.EventMessageUpdated && msg.Payload.SessionID == m.app.CurrentSession.ID {
for i, v := range m.messages {
if v.ID == msg.Payload.ID {
m.messages[i] = msg.Payload
delete(m.cachedContent, msg.Payload.ID)
needsRerender = true
break
}
}
}
if needsRerender {
m.renderView()
if len(m.messages) > 0 {
if (msg.Type == message.EventMessageCreated) ||
(msg.Type == message.EventMessageUpdated && msg.Payload.ID == m.messages[len(m.messages)-1].ID) {
m.viewport.GotoBottom()
}
}
}
// case pubsub.Event[message.Message]:
// needsRerender := false
// if msg.Type == message.EventMessageCreated {
// if msg.Payload.SessionID == m.app.CurrentSessionOLD.ID {
// messageExists := false
// for _, v := range m.messages {
// if v.ID == msg.Payload.ID {
// messageExists = true
// break
// }
// }
//
// if !messageExists {
// if len(m.messages) > 0 {
// lastMsgID := m.messages[len(m.messages)-1].ID
// delete(m.cachedContent, lastMsgID)
// }
//
// m.messages = append(m.messages, msg.Payload)
// delete(m.cachedContent, m.currentMsgID)
// m.currentMsgID = msg.Payload.ID
// needsRerender = true
// }
// }
// // There are tool calls from the child task
// for _, v := range m.messages {
// for _, c := range v.ToolCalls() {
// if c.ID == msg.Payload.SessionID {
// delete(m.cachedContent, v.ID)
// needsRerender = true
// }
// }
// }
// } else if msg.Type == message.EventMessageUpdated && msg.Payload.SessionID == m.app.CurrentSessionOLD.ID {
// for i, v := range m.messages {
// if v.ID == msg.Payload.ID {
// m.messages[i] = msg.Payload
// delete(m.cachedContent, msg.Payload.ID)
// needsRerender = true
// break
// }
// }
// }
// if needsRerender {
// m.renderView()
// if len(m.messages) > 0 {
// if (msg.Type == message.EventMessageCreated) ||
// (msg.Type == message.EventMessageUpdated && msg.Payload.ID == m.messages[len(m.messages)-1].ID) {
// m.viewport.GotoBottom()
// }
// }
// }
}
spinner, cmd := m.spinner.Update(msg)
@@ -169,7 +168,7 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (m *messagesCmp) IsAgentWorking() bool {
return m.app.PrimaryAgent.IsSessionBusy(m.app.CurrentSession.ID)
return m.app.PrimaryAgentOLD.IsSessionBusy(m.app.CurrentSessionOLD.ID)
}
func formatTimeDifference(unixTime1, unixTime2 int64) string {
@@ -192,48 +191,48 @@ func (m *messagesCmp) renderView() {
if m.width == 0 {
return
}
for inx, msg := range m.messages {
for _, msg := range m.app.Messages {
switch msg.Role {
case message.User:
if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
case client.User:
if cache, ok := m.cachedContent[msg.Id]; ok && cache.width == m.width {
m.uiMessages = append(m.uiMessages, cache.content...)
continue
}
userMsg := renderUserMessage(
msg,
msg.ID == m.currentMsgID,
msg.Id == m.currentMsgID,
m.width,
pos,
)
m.uiMessages = append(m.uiMessages, userMsg)
m.cachedContent[msg.ID] = cacheItem{
m.cachedContent[msg.Id] = cacheItem{
width: m.width,
content: []uiMessage{userMsg},
}
pos += userMsg.height + 1 // + 1 for spacing
case message.Assistant:
if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
// pos += userMsg.height + 1 // + 1 for spacing
case client.Assistant:
if cache, ok := m.cachedContent[msg.Id]; ok && cache.width == m.width {
m.uiMessages = append(m.uiMessages, cache.content...)
continue
}
assistantMessages := renderAssistantMessage(
msg,
inx,
m.messages,
m.app.Messages,
m.currentMsgID,
m.width,
pos,
m.showToolMessages,
)
for _, msg := range assistantMessages {
m.uiMessages = append(m.uiMessages, msg)
pos += msg.height + 1 // + 1 for spacing
}
m.cachedContent[msg.ID] = cacheItem{
width: m.width,
content: assistantMessages,
}
// assistantMessages := renderAssistantMessage(
// msg,
// inx,
// m.app.Messages,
// m.app.MessagesOLD,
// m.currentMsgID,
// m.width,
// pos,
// m.showToolMessages,
// )
// for _, msg := range assistantMessages {
// m.uiMessages = append(m.uiMessages, msg)
// // pos += msg.height + 1 // + 1 for spacing
// }
// m.cachedContent[msg.Id] = cacheItem{
// width: m.width,
// content: assistantMessages,
// }
}
}
@@ -248,33 +247,23 @@ func (m *messagesCmp) renderView() {
)
}
temp, _ := json.MarshalIndent(m.app.State, "", " ")
// temp, _ := json.MarshalIndent(m.app.State, "", " ")
m.viewport.SetContent(
baseStyle.
Width(m.width).
Render(
string(temp),
// lipgloss.JoinVertical(
// lipgloss.Top,
// messages...,
// ),
// string(temp),
lipgloss.JoinVertical(
lipgloss.Top,
messages...,
),
),
)
}
func (m *messagesCmp) View() string {
baseStyle := styles.BaseStyle()
return baseStyle.
Width(m.width).
Render(
lipgloss.JoinVertical(
lipgloss.Top,
m.viewport.View(),
m.working(),
m.help(),
),
)
if m.rendering {
return baseStyle.
@@ -283,12 +272,12 @@ func (m *messagesCmp) View() string {
lipgloss.JoinVertical(
lipgloss.Top,
"Loading...",
m.working(),
// m.working(),
m.help(),
),
)
}
if len(m.messages) == 0 {
if len(m.app.Messages) == 0 {
content := baseStyle.
Width(m.width).
Height(m.height - 1).
@@ -314,7 +303,7 @@ func (m *messagesCmp) View() string {
lipgloss.JoinVertical(
lipgloss.Top,
m.viewport.View(),
m.working(),
// m.working(),
m.help(),
),
)
@@ -356,31 +345,31 @@ func hasUnfinishedToolCalls(messages []message.Message) bool {
return false
}
func (m *messagesCmp) working() string {
text := ""
if m.IsAgentWorking() && len(m.messages) > 0 {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
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..."
}
if task != "" {
text += baseStyle.
Width(m.width).
Foreground(t.Primary()).
Bold(true).
Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
}
}
return text
}
// func (m *messagesCmp) working() string {
// text := ""
// if m.IsAgentWorking() && len(m.app.Messages) > 0 {
// t := theme.CurrentTheme()
// baseStyle := styles.BaseStyle()
//
// task := "Thinking..."
// lastMessage := m.app.Messages[len(m.app.Messages)-1]
// if hasToolsWithoutResponse(m.app.Messages) {
// task = "Waiting for tool response..."
// } else if hasUnfinishedToolCalls(m.app.Messages) {
// task = "Building tool call..."
// } else if !lastMessage.IsFinished() {
// task = "Generating..."
// }
// if task != "" {
// text += baseStyle.
// Width(m.width).
// Foreground(t.Primary()).
// Bold(true).
// Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
// }
// }
// return text
// }
func (m *messagesCmp) help() string {
t := theme.CurrentTheme()
@@ -388,7 +377,7 @@ func (m *messagesCmp) help() string {
text := ""
if m.app.PrimaryAgent.IsBusy() {
if m.app.PrimaryAgentOLD.IsBusy() {
text += lipgloss.JoinHorizontal(
lipgloss.Left,
baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
@@ -429,8 +418,8 @@ func (m *messagesCmp) initialScreen() string {
}
func (m *messagesCmp) rerender() {
for _, msg := range m.messages {
delete(m.cachedContent, msg.ID)
for _, msg := range m.app.Messages {
delete(m.cachedContent, msg.Id)
}
m.renderView()
}
@@ -454,14 +443,16 @@ func (m *messagesCmp) GetSize() (int, int) {
}
func (m *messagesCmp) Reload(session *session.Session) tea.Cmd {
messages, err := m.app.Messages.List(context.Background(), session.ID)
if err != nil {
status.Error(err.Error())
return nil
}
m.messages = messages
if len(m.messages) > 0 {
m.currentMsgID = m.messages[len(m.messages)-1].ID
// messages := m.app.Messages
// messages, err := m.app.MessagesOLD.List(context.Background(), session.ID)
// if err != nil {
// status.Error(err.Error())
// return nil
// }
// m.messages = messages
if len(m.app.Messages) > 0 {
m.currentMsgID = m.app.Messages[len(m.app.Messages)-1].Id
}
delete(m.cachedContent, m.currentMsgID)
m.rendering = true

View File

@@ -86,7 +86,7 @@ func (m *sidebarCmp) sessionSection() string {
sessionValue := baseStyle.
Foreground(t.Text()).
Render(fmt.Sprintf(": %s", m.app.CurrentSession.Title))
Render(fmt.Sprintf(": %s", m.app.CurrentSessionOLD.Title))
return sessionKey + sessionValue
}
@@ -209,7 +209,7 @@ func NewSidebarCmp(app *app.App) tea.Model {
}
func (m *sidebarCmp) loadModifiedFiles(ctx context.Context) {
if m.app.CurrentSession.ID == "" {
if m.app.CurrentSessionOLD.ID == "" {
return
}

View File

@@ -9,7 +9,6 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/sst/opencode/internal/config"
"github.com/sst/opencode/internal/llm/models"
"github.com/sst/opencode/internal/lsp"
"github.com/sst/opencode/internal/lsp/protocol"
"github.com/sst/opencode/internal/pubsub"
"github.com/sst/opencode/internal/status"
@@ -155,8 +154,8 @@ func (m statusCmp) View() string {
// Initialize the help widget
status := getHelpWidget("")
if m.app.CurrentSession.ID != "" {
tokens := formatTokensAndCost(m.app.CurrentSession.PromptTokens+m.app.CurrentSession.CompletionTokens, model.ContextWindow, m.app.CurrentSession.Cost)
if m.app.CurrentSessionOLD.ID != "" {
tokens := formatTokensAndCost(m.app.CurrentSessionOLD.PromptTokens+m.app.CurrentSessionOLD.CompletionTokens, model.ContextWindow, m.app.CurrentSessionOLD.Cost)
tokensStyle := styles.Padded().
Background(t.Text()).
Foreground(t.BackgroundSecondary()).
@@ -245,12 +244,12 @@ func (m *statusCmp) projectDiagnostics() string {
// Check if any LSP server is still initializing
initializing := false
for _, client := range m.app.LSPClients {
if client.GetServerState() == lsp.StateStarting {
initializing = true
break
}
}
// for _, client := range m.app.LSPClients {
// if client.GetServerState() == lsp.StateStarting {
// initializing = true
// break
// }
// }
// If any server is initializing, show that status
if initializing {
@@ -263,22 +262,22 @@ func (m *statusCmp) projectDiagnostics() string {
warnDiagnostics := []protocol.Diagnostic{}
hintDiagnostics := []protocol.Diagnostic{}
infoDiagnostics := []protocol.Diagnostic{}
for _, client := range m.app.LSPClients {
for _, d := range client.GetDiagnostics() {
for _, diag := range d {
switch diag.Severity {
case protocol.SeverityError:
errorDiagnostics = append(errorDiagnostics, diag)
case protocol.SeverityWarning:
warnDiagnostics = append(warnDiagnostics, diag)
case protocol.SeverityHint:
hintDiagnostics = append(hintDiagnostics, diag)
case protocol.SeverityInformation:
infoDiagnostics = append(infoDiagnostics, diag)
}
}
}
}
// for _, client := range m.app.LSPClients {
// for _, d := range client.GetDiagnostics() {
// for _, diag := range d {
// switch diag.Severity {
// case protocol.SeverityError:
// errorDiagnostics = append(errorDiagnostics, diag)
// case protocol.SeverityWarning:
// warnDiagnostics = append(warnDiagnostics, diag)
// case protocol.SeverityHint:
// hintDiagnostics = append(hintDiagnostics, diag)
// case protocol.SeverityInformation:
// infoDiagnostics = append(infoDiagnostics, diag)
// }
// }
// }
// }
if len(errorDiagnostics) == 0 &&
len(warnDiagnostics) == 0 &&

View File

@@ -78,7 +78,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
case dialog.CommandRunCustomMsg:
// Check if the agent is busy before executing custom commands
if p.app.PrimaryAgent.IsBusy() {
if p.app.PrimaryAgentOLD.IsBusy() {
status.Warn("Agent is busy, please wait before executing a command...")
return p, nil
}
@@ -105,20 +105,20 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmd := p.setSidebar()
cmds = append(cmds, cmd)
case state.CompactSessionMsg:
if p.app.CurrentSession.ID == "" {
if p.app.CurrentSessionOLD.ID == "" {
status.Warn("No active session to compact.")
return p, nil
}
// Run compaction in background
go func(sessionID string) {
err := p.app.PrimaryAgent.CompactSession(context.Background(), sessionID, false)
err := p.app.PrimaryAgentOLD.CompactSession(context.Background(), sessionID, false)
if err != nil {
status.Error(fmt.Sprintf("Compaction failed: %v", err))
} else {
status.Info("Conversation compacted successfully.")
}
}(p.app.CurrentSession.ID)
}(p.app.CurrentSessionOLD.ID)
return p, nil
case dialog.CompletionDialogCloseMsg:
@@ -131,16 +131,16 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
p.app.SetCompletionDialogOpen(true)
// Continue sending keys to layout->chat
case key.Matches(msg, keyMap.NewSession):
p.app.CurrentSession = &session.Session{}
p.app.CurrentSessionOLD = &session.Session{}
return p, tea.Batch(
p.clearSidebar(),
util.CmdHandler(state.SessionClearedMsg{}),
)
case key.Matches(msg, keyMap.Cancel):
if p.app.CurrentSession.ID != "" {
if p.app.CurrentSessionOLD.ID != "" {
// Cancel the current session's generation process
// This allows users to interrupt long-running operations
p.app.PrimaryAgent.Cancel(p.app.CurrentSession.ID)
p.app.PrimaryAgentOLD.Cancel(p.app.CurrentSessionOLD.ID)
return p, nil
}
case key.Matches(msg, keyMap.ToggleTools):

View File

@@ -2,6 +2,7 @@ package tui
import (
"context"
"encoding/json"
"log/slog"
"strings"
@@ -178,7 +179,7 @@ func (a appModel) Init() tea.Cmd {
func (a appModel) updateAllPages(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
var cmd tea.Cmd
for id, _ := range a.pages {
for id := range a.pages {
a.pages[id], cmd = a.pages[id].Update(msg)
cmds = append(cmds, cmd)
}
@@ -256,26 +257,76 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, a.moveToPage(msg.ID)
case state.SessionSelectedMsg:
a.app.CurrentSession = msg
a.app.CurrentSessionOLD = msg
return a.updateAllPages(msg)
case pubsub.Event[session.Session]:
if msg.Type == session.EventSessionUpdated {
if a.app.CurrentSession.ID == msg.Payload.ID {
a.app.CurrentSession = &msg.Payload
if a.app.CurrentSessionOLD.ID == msg.Payload.ID {
a.app.CurrentSessionOLD = &msg.Payload
}
}
// Handle SSE events from the TypeScript backend
case client.EventStorageWrite:
slog.Debug("Received SSE event", "key", msg.Properties.Key)
parts := strings.Split(msg.Key, "/")
if len(parts) < 3 {
return a, nil
}
if parts[0] == "session" && parts[1] == "info" {
sessionId := parts[2]
if sessionId == a.app.Session.Id {
var sessionInfo client.SessionInfo
bytes, _ := json.Marshal(msg.Content)
if err := json.Unmarshal(bytes, &sessionInfo); err != nil {
status.Error(err.Error())
return a, nil
}
a.app.Session = &sessionInfo
}
return a, nil
}
if parts[0] == "session" && parts[1] == "message" {
sessionId := parts[2]
if sessionId == a.app.Session.Id {
messageId := parts[3]
var message client.SessionMessage
bytes, _ := json.Marshal(msg.Content)
if err := json.Unmarshal(bytes, &message); err != nil {
status.Error(err.Error())
return a, nil
}
for i, m := range a.app.Messages {
if m.Id == messageId {
a.app.Messages[i] = message
slog.Debug("Updated message", "message", message)
return a, nil
}
}
a.app.Messages = append(a.app.Messages, message)
slog.Debug("Appended message", "message", message)
// a.app.CurrentSession.MessageCount++
// a.app.CurrentSession.PromptTokens += message.PromptTokens
// a.app.CurrentSession.CompletionTokens += message.CompletionTokens
// a.app.CurrentSession.Cost += message.Cost
// a.app.CurrentSession.UpdatedAt = message.CreatedAt
}
return a, nil
}
// log key and content
slog.Debug("Received SSE event", "key", msg.Key, "content", msg.Content)
splits := strings.Split(msg.Properties.Key, "/")
current := a.app.State
for i, part := range splits {
if i == len(splits)-1 {
current[part] = msg.Properties.Content
for i, part := range parts {
if i == len(parts)-1 {
current[part] = msg.Content
} else {
if _, exists := current[part]; !exists {
current[part] = make(map[string]any)
@@ -566,7 +617,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return a, nil
case key.Matches(msg, helpEsc):
if a.app.PrimaryAgent.IsBusy() {
if a.app.PrimaryAgentOLD.IsBusy() {
if a.showQuit {
return a, nil
}
@@ -705,27 +756,9 @@ func (a *appModel) RegisterCommand(cmd dialog.Command) {
}
// getAvailableToolNames returns a list of all available tool names
func getAvailableToolNames(app *app.App) []string {
func getAvailableToolNames(_ *app.App) []string {
// TODO: Tools not implemented in API yet
return []string{"Tools not available in API mode"}
/*
// Get primary agent tools (which already include MCP tools)
allTools := agent.PrimaryAgentTools(
app.Permissions,
app.Sessions,
app.Messages,
app.History,
app.LSPClients,
)
// Extract tool names
var toolNames []string
for _, tool := range allTools {
toolNames = append(toolNames, tool.Info().Name)
}
return toolNames
*/
}
func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
@@ -785,7 +818,7 @@ func (a appModel) View() string {
}
if !a.app.PrimaryAgent.IsBusy() {
if !a.app.PrimaryAgentOLD.IsBusy() {
a.status.SetHelpWidgetMsg("ctrl+? help")
} else {
a.status.SetHelpWidgetMsg("? help")
@@ -799,7 +832,7 @@ func (a appModel) View() string {
if a.showPermissions {
bindings = append(bindings, a.permissions.BindingKeys()...)
}
if !a.app.PrimaryAgent.IsBusy() {
if !a.app.PrimaryAgentOLD.IsBusy() {
bindings = append(bindings, helpEsc)
}
a.help.SetBindings(bindings)