From a5da5127faffacd7703fc0dde061ef1f490d3dce Mon Sep 17 00:00:00 2001 From: adamdottv <2363879+adamdottv@users.noreply.github.com> Date: Tue, 17 Jun 2025 07:09:04 -0500 Subject: chore: consolidate chat page into tui.go --- packages/tui/internal/components/chat/editor.go | 13 +- packages/tui/internal/components/chat/messages.go | 21 +++- packages/tui/internal/components/core/status.go | 140 --------------------- .../tui/internal/components/dialog/complete.go | 4 +- packages/tui/internal/components/dialog/models.go | 3 +- .../tui/internal/components/dialog/permission.go | 4 +- packages/tui/internal/components/dialog/session.go | 3 +- packages/tui/internal/components/dialog/theme.go | 6 +- packages/tui/internal/components/list/list.go | 4 +- packages/tui/internal/components/status/status.go | 140 +++++++++++++++++++++ 10 files changed, 175 insertions(+), 163 deletions(-) delete mode 100644 packages/tui/internal/components/core/status.go create mode 100644 packages/tui/internal/components/status/status.go (limited to 'packages/tui/internal/components') diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index 349087fbe..a2d33f172 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -16,12 +16,17 @@ import ( "github.com/sst/opencode/internal/commands" "github.com/sst/opencode/internal/components/dialog" "github.com/sst/opencode/internal/image" - "github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" ) +type EditorComponent interface { + tea.Model + tea.ViewModel + Value() string +} + type editorComponent struct { width int height int @@ -99,7 +104,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd var cmd tea.Cmd switch msg := msg.(type) { - case dialog.ThemeChangedMsg: + case dialog.ThemeSelectedMsg: m.textarea = createTextArea(&m.textarea) m.spinner = createSpinner() return m, m.spinner.Tick @@ -434,11 +439,11 @@ func createSpinner() spinner.Model { ) } -func (m *editorComponent) GetValue() string { +func (m *editorComponent) Value() string { return m.textarea.Value() } -func NewEditorComponent(app *app.App) layout.ModelWithView { +func NewEditorComponent(app *app.App) EditorComponent { s := createSpinner() ta := createTextArea(nil) diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index d0d839845..37e3380ca 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -12,12 +12,16 @@ import ( "github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/components/dialog" "github.com/sst/opencode/internal/layout" - "github.com/sst/opencode/internal/state" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/pkg/client" ) +type MessagesComponent interface { + tea.Model + tea.ViewModel +} + type messagesComponent struct { app *app.App width, height int @@ -69,7 +73,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewport.GotoBottom() m.tail = true return m, nil - case dialog.ThemeChangedMsg: + case dialog.ThemeSelectedMsg: m.cache.Clear() m.renderView() return m, nil @@ -77,12 +81,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.showToolResults = !m.showToolResults m.renderView() return m, nil - case state.SessionSelectedMsg: + case app.SessionSelectedMsg: m.cache.Clear() cmd := m.Reload() m.viewport.GotoBottom() return m, cmd - case state.SessionClearedMsg: + case app.SessionClearedMsg: m.cache.Clear() cmd := m.Reload() return m, cmd @@ -101,7 +105,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.tail { m.viewport.GotoBottom() } - case state.StateUpdatedMsg: + case client.EventSessionUpdated: + m.renderView() + if m.tail { + m.viewport.GotoBottom() + } + case client.EventMessageUpdated: m.renderView() if m.tail { m.viewport.GotoBottom() @@ -389,7 +398,7 @@ func (m *messagesComponent) Reload() tea.Cmd { } } -func NewMessagesComponent(app *app.App) layout.ModelWithView { +func NewMessagesComponent(app *app.App) MessagesComponent { customSpinner := spinner.Spinner{ Frames: []string{" ", "┃", "┃"}, FPS: time.Second / 3, diff --git a/packages/tui/internal/components/core/status.go b/packages/tui/internal/components/core/status.go deleted file mode 100644 index 9540c6c2b..000000000 --- a/packages/tui/internal/components/core/status.go +++ /dev/null @@ -1,140 +0,0 @@ -package core - -import ( - "fmt" - "strings" - - tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/lipgloss/v2" - "github.com/sst/opencode/internal/app" - "github.com/sst/opencode/internal/layout" - "github.com/sst/opencode/internal/styles" - "github.com/sst/opencode/internal/theme" -) - -type StatusComponent interface { - layout.ModelWithView -} - -type statusComponent struct { - app *app.App - width int -} - -func (m statusComponent) Init() tea.Cmd { - return nil -} - -func (m statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.width = msg.Width - return m, nil - } - return m, nil -} - -func (m statusComponent) logo() string { - t := theme.CurrentTheme() - base := lipgloss.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render - emphasis := lipgloss.NewStyle().Bold(true).Background(t.BackgroundElement()).Foreground(t.Text()).Render - - open := base("open") - code := emphasis("code ") - version := base(m.app.Version) - return styles.Padded(). - Background(t.BackgroundElement()). - Render(open + code + version) -} - -func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) string { - // Format tokens in human-readable format (e.g., 110K, 1.2M) - var formattedTokens string - switch { - case tokens >= 1_000_000: - formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000) - case tokens >= 1_000: - formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000) - default: - formattedTokens = fmt.Sprintf("%d", int(tokens)) - } - - // Remove .0 suffix if present - if strings.HasSuffix(formattedTokens, ".0K") { - formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1) - } - if strings.HasSuffix(formattedTokens, ".0M") { - formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1) - } - - // Format cost with $ symbol and 2 decimal places - formattedCost := fmt.Sprintf("$%.2f", cost) - percentage := (float64(tokens) / float64(contextWindow)) * 100 - - return fmt.Sprintf("Tokens: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost) -} - -func (m statusComponent) View() string { - t := theme.CurrentTheme() - if m.app.Session.Id == "" { - return styles.BaseStyle(). - Background(t.Background()). - Width(m.width). - Height(2). - Render("") - } - - logo := m.logo() - - cwd := styles.Padded(). - Foreground(t.TextMuted()). - Background(t.BackgroundSubtle()). - Render(m.app.Info.Path.Cwd) - - sessionInfo := "" - if m.app.Session.Id != "" { - tokens := float32(0) - cost := float32(0) - contextWindow := m.app.Model.Limit.Context - - for _, message := range m.app.Messages { - if message.Metadata.Assistant != nil { - cost += message.Metadata.Assistant.Cost - usage := message.Metadata.Assistant.Tokens - if usage.Output > 0 { - tokens = (usage.Input + - usage.Cache.Write + - usage.Cache.Read + - usage.Output + - usage.Reasoning) - } - } - } - - sessionInfo = styles.Padded(). - Background(t.BackgroundElement()). - Foreground(t.TextMuted()). - Render(formatTokensAndCost(tokens, contextWindow, cost)) - } - - // diagnostics := styles.Padded().Background(t.BackgroundElement()).Render(m.projectDiagnostics()) - - space := max( - 0, - m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo), - ) - spacer := lipgloss.NewStyle().Background(t.BackgroundSubtle()).Width(space).Render("") - - status := logo + cwd + spacer + sessionInfo - - blank := styles.BaseStyle().Background(t.Background()).Width(m.width).Render("") - return blank + "\n" + status -} - -func NewStatusCmp(app *app.App) StatusComponent { - statusComponent := &statusComponent{ - app: app, - } - - return statusComponent -} diff --git a/packages/tui/internal/components/dialog/complete.go b/packages/tui/internal/components/dialog/complete.go index d87a331cf..ca86b00e6 100644 --- a/packages/tui/internal/components/dialog/complete.go +++ b/packages/tui/internal/components/dialog/complete.go @@ -6,7 +6,6 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" "github.com/sst/opencode/internal/components/list" - "github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" @@ -76,7 +75,8 @@ type CompletionDialogCompleteItemMsg struct { type CompletionDialogCloseMsg struct{} type CompletionDialog interface { - layout.ModelWithView + tea.Model + tea.ViewModel SetWidth(width int) IsEmpty() bool SetProvider(provider CompletionProvider) diff --git a/packages/tui/internal/components/dialog/models.go b/packages/tui/internal/components/dialog/models.go index 616347088..dfb11dffb 100644 --- a/packages/tui/internal/components/dialog/models.go +++ b/packages/tui/internal/components/dialog/models.go @@ -13,7 +13,6 @@ import ( "github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/components/modal" "github.com/sst/opencode/internal/layout" - "github.com/sst/opencode/internal/state" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" @@ -115,7 +114,7 @@ func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Sequence( util.CmdHandler(modal.CloseModalMsg{}), util.CmdHandler( - state.ModelSelectedMsg{ + app.ModelSelectedMsg{ Provider: m.provider, Model: models[m.selectedIdx], }), diff --git a/packages/tui/internal/components/dialog/permission.go b/packages/tui/internal/components/dialog/permission.go index ba82c876b..1f573e59d 100644 --- a/packages/tui/internal/components/dialog/permission.go +++ b/packages/tui/internal/components/dialog/permission.go @@ -6,7 +6,6 @@ import ( "github.com/charmbracelet/bubbles/v2/viewport" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" - "github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" @@ -30,7 +29,8 @@ type PermissionResponseMsg struct { // PermissionDialogComponent interface for permission dialog component type PermissionDialogComponent interface { - layout.ModelWithView + tea.Model + tea.ViewModel // SetPermissions(permission permission.PermissionRequest) tea.Cmd } diff --git a/packages/tui/internal/components/dialog/session.go b/packages/tui/internal/components/dialog/session.go index 48278d85e..b59ebe3d5 100644 --- a/packages/tui/internal/components/dialog/session.go +++ b/packages/tui/internal/components/dialog/session.go @@ -8,7 +8,6 @@ import ( "github.com/sst/opencode/internal/components/list" "github.com/sst/opencode/internal/components/modal" "github.com/sst/opencode/internal/layout" - "github.com/sst/opencode/internal/state" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" @@ -69,7 +68,7 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) { s.selectedSessionID = selectedSession.Id return s, tea.Sequence( util.CmdHandler(modal.CloseModalMsg{}), - util.CmdHandler(state.SessionSelectedMsg(&selectedSession)), + util.CmdHandler(app.SessionSelectedMsg(&selectedSession)), ) } } diff --git a/packages/tui/internal/components/dialog/theme.go b/packages/tui/internal/components/dialog/theme.go index 50472428b..63135bc81 100644 --- a/packages/tui/internal/components/dialog/theme.go +++ b/packages/tui/internal/components/dialog/theme.go @@ -10,8 +10,8 @@ import ( "github.com/sst/opencode/internal/util" ) -// ThemeChangedMsg is sent when the theme is changed -type ThemeChangedMsg struct { +// ThemeSelectedMsg is sent when the theme is changed +type ThemeSelectedMsg struct { ThemeName string } @@ -75,7 +75,7 @@ func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return t, tea.Sequence( util.CmdHandler(modal.CloseModalMsg{}), - util.CmdHandler(ThemeChangedMsg{ThemeName: selectedTheme}), + util.CmdHandler(ThemeSelectedMsg{ThemeName: selectedTheme}), ) } } diff --git a/packages/tui/internal/components/list/list.go b/packages/tui/internal/components/list/list.go index fea77e902..cefcaabef 100644 --- a/packages/tui/internal/components/list/list.go +++ b/packages/tui/internal/components/list/list.go @@ -4,7 +4,6 @@ import ( "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" - "github.com/sst/opencode/internal/layout" ) type ListItem interface { @@ -12,7 +11,8 @@ type ListItem interface { } type List[T ListItem] interface { - layout.ModelWithView + tea.Model + tea.ViewModel SetMaxWidth(maxWidth int) GetSelectedItem() (item T, idx int) SetItems(items []T) diff --git a/packages/tui/internal/components/status/status.go b/packages/tui/internal/components/status/status.go new file mode 100644 index 000000000..01a2659c6 --- /dev/null +++ b/packages/tui/internal/components/status/status.go @@ -0,0 +1,140 @@ +package status + +import ( + "fmt" + "strings" + + tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/lipgloss/v2" + "github.com/sst/opencode/internal/app" + "github.com/sst/opencode/internal/styles" + "github.com/sst/opencode/internal/theme" +) + +type StatusComponent interface { + tea.Model + tea.ViewModel +} + +type statusComponent struct { + app *app.App + width int +} + +func (m statusComponent) Init() tea.Cmd { + return nil +} + +func (m statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + return m, nil + } + return m, nil +} + +func (m statusComponent) logo() string { + t := theme.CurrentTheme() + base := lipgloss.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render + emphasis := lipgloss.NewStyle().Bold(true).Background(t.BackgroundElement()).Foreground(t.Text()).Render + + open := base("open") + code := emphasis("code ") + version := base(m.app.Version) + return styles.Padded(). + Background(t.BackgroundElement()). + Render(open + code + version) +} + +func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) string { + // Format tokens in human-readable format (e.g., 110K, 1.2M) + var formattedTokens string + switch { + case tokens >= 1_000_000: + formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000) + case tokens >= 1_000: + formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000) + default: + formattedTokens = fmt.Sprintf("%d", int(tokens)) + } + + // Remove .0 suffix if present + if strings.HasSuffix(formattedTokens, ".0K") { + formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1) + } + if strings.HasSuffix(formattedTokens, ".0M") { + formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1) + } + + // Format cost with $ symbol and 2 decimal places + formattedCost := fmt.Sprintf("$%.2f", cost) + percentage := (float64(tokens) / float64(contextWindow)) * 100 + + return fmt.Sprintf("Tokens: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost) +} + +func (m statusComponent) View() string { + t := theme.CurrentTheme() + if m.app.Session.Id == "" { + return styles.BaseStyle(). + Background(t.Background()). + Width(m.width). + Height(2). + Render("") + } + + logo := m.logo() + + cwd := styles.Padded(). + Foreground(t.TextMuted()). + Background(t.BackgroundSubtle()). + Render(m.app.Info.Path.Cwd) + + sessionInfo := "" + if m.app.Session.Id != "" { + tokens := float32(0) + cost := float32(0) + contextWindow := m.app.Model.Limit.Context + + for _, message := range m.app.Messages { + if message.Metadata.Assistant != nil { + cost += message.Metadata.Assistant.Cost + usage := message.Metadata.Assistant.Tokens + if usage.Output > 0 { + tokens = (usage.Input + + usage.Cache.Write + + usage.Cache.Read + + usage.Output + + usage.Reasoning) + } + } + } + + sessionInfo = styles.Padded(). + Background(t.BackgroundElement()). + Foreground(t.TextMuted()). + Render(formatTokensAndCost(tokens, contextWindow, cost)) + } + + // diagnostics := styles.Padded().Background(t.BackgroundElement()).Render(m.projectDiagnostics()) + + space := max( + 0, + m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo), + ) + spacer := lipgloss.NewStyle().Background(t.BackgroundSubtle()).Width(space).Render("") + + status := logo + cwd + spacer + sessionInfo + + blank := styles.BaseStyle().Background(t.Background()).Width(m.width).Render("") + return blank + "\n" + status +} + +func NewStatusCmp(app *app.App) StatusComponent { + statusComponent := &statusComponent{ + app: app, + } + + return statusComponent +} -- cgit v1.2.3