diff options
| author | adamdottv <[email protected]> | 2025-05-28 15:36:31 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-05-28 15:36:36 -0500 |
| commit | 9d7c5efb9b0b60c62aef3777b65b458a31ebbc88 (patch) | |
| tree | 0f5acb5b8093d872b30178ded53df719be40cf44 /internal/tui/components | |
| parent | 8863a499a9e311a48d6ab8bc05d267fb2a01f060 (diff) | |
| download | opencode-9d7c5efb9b0b60c62aef3777b65b458a31ebbc88.tar.gz opencode-9d7c5efb9b0b60c62aef3777b65b458a31ebbc88.zip | |
wip: refactoring tui
Diffstat (limited to 'internal/tui/components')
| -rw-r--r-- | internal/tui/components/chat/editor.go | 26 | ||||
| -rw-r--r-- | internal/tui/components/chat/messages.go | 2 | ||||
| -rw-r--r-- | internal/tui/components/chat/sidebar.go | 178 | ||||
| -rw-r--r-- | internal/tui/components/core/status.go | 2 | ||||
| -rw-r--r-- | internal/tui/components/dialog/filepicker.go | 4 | ||||
| -rw-r--r-- | internal/tui/components/logs/details.go | 187 | ||||
| -rw-r--r-- | internal/tui/components/logs/table.go | 207 |
7 files changed, 106 insertions, 500 deletions
diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index 0858e22df..3b82c96d3 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -13,9 +13,9 @@ import ( "github.com/charmbracelet/bubbles/textarea" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/message" "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/image" "github.com/sst/opencode/internal/tui/layout" @@ -37,10 +37,10 @@ type editorCmp struct { } type EditorKeyMaps struct { - Send key.Binding - OpenEditor key.Binding - Paste key.Binding - HistoryUp key.Binding + Send key.Binding + OpenEditor key.Binding + Paste key.Binding + HistoryUp key.Binding HistoryDown key.Binding } @@ -251,14 +251,14 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryUp) && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() { // Get the current line number currentLine := m.textarea.Line() - + // Only navigate history if we're at the first line if currentLine == 0 && len(m.history) > 0 { // Save current message if we're just starting to navigate if m.historyIndex == len(m.history) { m.currentMessage = m.textarea.Value() } - + // Go to previous message in history if m.historyIndex > 0 { m.historyIndex-- @@ -267,14 +267,14 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } } - + if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryDown) && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() { // Get the current line number and total lines currentLine := m.textarea.Line() value := m.textarea.Value() lines := strings.Split(value, "\n") totalLines := len(lines) - + // Only navigate history if we're at the last line if currentLine == totalLines-1 { if m.historyIndex < len(m.history)-1 { @@ -403,10 +403,10 @@ func CreateTextArea(existing *textarea.Model) textarea.Model { func NewEditorCmp(app *app.App) tea.Model { ta := CreateTextArea(nil) return &editorCmp{ - app: app, - textarea: ta, - history: []string{}, - historyIndex: 0, + app: app, + textarea: ta, + history: []string{}, + historyIndex: 0, currentMessage: "", } } diff --git a/internal/tui/components/chat/messages.go b/internal/tui/components/chat/messages.go index 668f0f6cd..ce02e2e07 100644 --- a/internal/tui/components/chat/messages.go +++ b/internal/tui/components/chat/messages.go @@ -12,11 +12,11 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/sst/opencode/internal/app" "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" diff --git a/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go index c2b392c24..6676f9b09 100644 --- a/internal/tui/components/chat/sidebar.go +++ b/internal/tui/components/chat/sidebar.go @@ -8,8 +8,8 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/config" + "github.com/sst/opencode/internal/tui/app" // "github.com/sst/opencode/internal/diff" "github.com/sst/opencode/internal/history" "github.com/sst/opencode/internal/pubsub" @@ -216,17 +216,17 @@ func (m *sidebarCmp) loadModifiedFiles(ctx context.Context) { // TODO: History service not implemented in API yet return /* - // Get all latest files for this session - latestFiles, err := m.app.History.ListLatestSessionFiles(ctx, m.app.CurrentSession.ID) - if err != nil { - return - } + // Get all latest files for this session + latestFiles, err := m.app.History.ListLatestSessionFiles(ctx, m.app.CurrentSession.ID) + if err != nil { + return + } - // Get all files for this session (to find initial versions) - allFiles, err := m.app.History.ListBySession(ctx, m.app.CurrentSession.ID) - if err != nil { - return - } + // Get all files for this session (to find initial versions) + allFiles, err := m.app.History.ListBySession(ctx, m.app.CurrentSession.ID) + if err != nil { + return + } */ // Clear the existing map to rebuild it @@ -236,28 +236,75 @@ func (m *sidebarCmp) loadModifiedFiles(ctx context.Context) { }) /* - // Process each latest file - for _, file := range latestFiles { + // Process each latest file + for _, file := range latestFiles { + // Skip if this is the initial version (no changes to show) + if file.Version == history.InitialVersion { + continue + } + + // Find the initial version for this specific file + var initialVersion history.File + for _, v := range allFiles { + if v.Path == file.Path && v.Version == history.InitialVersion { + initialVersion = v + break + } + } + + // Skip if we can't find the initial version + if initialVersion.ID == "" { + continue + } + if initialVersion.Content == file.Content { + continue + } + + // Calculate diff between initial and latest version + _, additions, removals := diff.GenerateDiff(initialVersion.Content, file.Content, file.Path) + + // Only add to modified files if there are changes + if additions > 0 || removals > 0 { + // Remove working directory prefix from file path + displayPath := file.Path + workingDir := config.WorkingDirectory() + displayPath = strings.TrimPrefix(displayPath, workingDir) + displayPath = strings.TrimPrefix(displayPath, "/") + + m.modFiles[displayPath] = struct { + additions int + removals int + }{ + additions: additions, + removals: removals, + } + } + } + */ +} + +func (m *sidebarCmp) processFileChanges(ctx context.Context, file history.File) { + // TODO: History service not implemented in API yet + return + /* // Skip if this is the initial version (no changes to show) if file.Version == history.InitialVersion { - continue + return } - // Find the initial version for this specific file - var initialVersion history.File - for _, v := range allFiles { - if v.Path == file.Path && v.Version == history.InitialVersion { - initialVersion = v - break - } + // Find the initial version for this file + initialVersion, err := m.findInitialVersion(ctx, file.Path) + if err != nil || initialVersion.ID == "" { + return } - // Skip if we can't find the initial version - if initialVersion.ID == "" { - continue - } + // Skip if content hasn't changed if initialVersion.Content == file.Content { - continue + // If this file was previously modified but now matches the initial version, + // remove it from the modified files list + displayPath := getDisplayPath(file.Path) + delete(m.modFiles, displayPath) + return } // Calculate diff between initial and latest version @@ -265,12 +312,7 @@ func (m *sidebarCmp) loadModifiedFiles(ctx context.Context) { // Only add to modified files if there are changes if additions > 0 || removals > 0 { - // Remove working directory prefix from file path - displayPath := file.Path - workingDir := config.WorkingDirectory() - displayPath = strings.TrimPrefix(displayPath, workingDir) - displayPath = strings.TrimPrefix(displayPath, "/") - + displayPath := getDisplayPath(file.Path) m.modFiles[displayPath] = struct { additions int removals int @@ -278,53 +320,11 @@ func (m *sidebarCmp) loadModifiedFiles(ctx context.Context) { additions: additions, removals: removals, } + } else { + // If no changes, remove from modified files + displayPath := getDisplayPath(file.Path) + delete(m.modFiles, displayPath) } - } - */ -} - -func (m *sidebarCmp) processFileChanges(ctx context.Context, file history.File) { - // TODO: History service not implemented in API yet - return - /* - // Skip if this is the initial version (no changes to show) - if file.Version == history.InitialVersion { - return - } - - // Find the initial version for this file - initialVersion, err := m.findInitialVersion(ctx, file.Path) - if err != nil || initialVersion.ID == "" { - return - } - - // Skip if content hasn't changed - if initialVersion.Content == file.Content { - // If this file was previously modified but now matches the initial version, - // remove it from the modified files list - displayPath := getDisplayPath(file.Path) - delete(m.modFiles, displayPath) - return - } - - // Calculate diff between initial and latest version - _, additions, removals := diff.GenerateDiff(initialVersion.Content, file.Content, file.Path) - - // Only add to modified files if there are changes - if additions > 0 || removals > 0 { - displayPath := getDisplayPath(file.Path) - m.modFiles[displayPath] = struct { - additions int - removals int - }{ - additions: additions, - removals: removals, - } - } else { - // If no changes, remove from modified files - displayPath := getDisplayPath(file.Path) - delete(m.modFiles, displayPath) - } */ } @@ -333,22 +333,22 @@ func (m *sidebarCmp) findInitialVersion(ctx context.Context, path string) (histo // TODO: History service not implemented in API yet return history.File{}, fmt.Errorf("history service not implemented") /* - // Get all versions of this file for the session - fileVersions, err := m.app.History.ListBySession(ctx, m.app.CurrentSession.ID) - if err != nil { - return history.File{}, err - } + // Get all versions of this file for the session + fileVersions, err := m.app.History.ListBySession(ctx, m.app.CurrentSession.ID) + if err != nil { + return history.File{}, err + } */ /* - // Find the initial version - for _, v := range fileVersions { - if v.Path == path && v.Version == history.InitialVersion { - return v, nil + // Find the initial version + for _, v := range fileVersions { + if v.Path == path && v.Version == history.InitialVersion { + return v, nil + } } - } - return history.File{}, fmt.Errorf("initial version not found") + return history.File{}, fmt.Errorf("initial version not found") */ } diff --git a/internal/tui/components/core/status.go b/internal/tui/components/core/status.go index 2ac48cfdc..681703c36 100644 --- a/internal/tui/components/core/status.go +++ b/internal/tui/components/core/status.go @@ -7,13 +7,13 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/sst/opencode/internal/app" "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" + "github.com/sst/opencode/internal/tui/app" "github.com/sst/opencode/internal/tui/styles" "github.com/sst/opencode/internal/tui/theme" ) diff --git a/internal/tui/components/dialog/filepicker.go b/internal/tui/components/dialog/filepicker.go index 77e64e16f..33f7599db 100644 --- a/internal/tui/components/dialog/filepicker.go +++ b/internal/tui/components/dialog/filepicker.go @@ -17,10 +17,10 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/config" "github.com/sst/opencode/internal/message" "github.com/sst/opencode/internal/status" + "github.com/sst/opencode/internal/tui/app" "github.com/sst/opencode/internal/tui/image" "github.com/sst/opencode/internal/tui/styles" "github.com/sst/opencode/internal/tui/theme" @@ -42,7 +42,7 @@ type FilePrickerKeyMap struct { OpenFilePicker key.Binding Esc key.Binding InsertCWD key.Binding - Paste key.Binding + Paste key.Binding } var filePickerKeyMap = FilePrickerKeyMap{ diff --git a/internal/tui/components/logs/details.go b/internal/tui/components/logs/details.go deleted file mode 100644 index bc59fdc6f..000000000 --- a/internal/tui/components/logs/details.go +++ /dev/null @@ -1,187 +0,0 @@ -package logs - -import ( - "bytes" - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/sst/opencode/internal/logging" - "github.com/sst/opencode/internal/tui/layout" - "github.com/sst/opencode/internal/tui/styles" - "github.com/sst/opencode/internal/tui/theme" -) - -type DetailComponent interface { - tea.Model - layout.Sizeable - layout.Bindings -} - -type detailCmp struct { - width, height int - currentLog logging.Log - viewport viewport.Model - focused bool -} - -func (i *detailCmp) Init() tea.Cmd { - return nil -} - -func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - switch msg := msg.(type) { - case selectedLogMsg: - if msg.ID != i.currentLog.ID { - i.currentLog = logging.Log(msg) - // Defer content update to avoid blocking the UI - cmd = tea.Tick(time.Millisecond*1, func(time.Time) tea.Msg { - i.updateContent() - return nil - }) - } - case tea.KeyMsg: - // Only process keyboard input when focused - if !i.focused { - return i, nil - } - // Handle keyboard input for scrolling - i.viewport, cmd = i.viewport.Update(msg) - return i, cmd - } - - return i, cmd -} - -func (i *detailCmp) updateContent() { - var content strings.Builder - t := theme.CurrentTheme() - - // Format the header with timestamp and level - timeStyle := lipgloss.NewStyle().Foreground(t.TextMuted()) - levelStyle := getLevelStyle(i.currentLog.Level) - - // Format timestamp - timeStr := i.currentLog.Timestamp.Format(time.RFC3339) - - header := lipgloss.JoinHorizontal( - lipgloss.Center, - timeStyle.Render(timeStr), - " ", - levelStyle.Render(i.currentLog.Level), - ) - - content.WriteString(lipgloss.NewStyle().Bold(true).Render(header)) - content.WriteString("\n\n") - - // Message with styling - messageStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text()) - content.WriteString(messageStyle.Render("Message:")) - content.WriteString("\n") - content.WriteString(lipgloss.NewStyle().Padding(0, 2).Width(i.width).Render(i.currentLog.Message)) - content.WriteString("\n\n") - - // Attributes section - if len(i.currentLog.Attributes) > 0 { - attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text()) - content.WriteString(attrHeaderStyle.Render("Attributes:")) - content.WriteString("\n") - - // Create a table-like display for attributes - keyStyle := lipgloss.NewStyle().Foreground(t.Primary()).Bold(true) - valueStyle := lipgloss.NewStyle().Foreground(t.Text()) - - for key, value := range i.currentLog.Attributes { - // if value is JSON, render it with indentation - if strings.HasPrefix(value, "{") { - var indented bytes.Buffer - if err := json.Indent(&indented, []byte(value), "", " "); err != nil { - indented.WriteString(value) - } - value = indented.String() - } - - attrLine := fmt.Sprintf("%s: %s", - keyStyle.Render(key), - valueStyle.Render(value), - ) - - content.WriteString(lipgloss.NewStyle().Padding(0, 2).Width(i.width).Render(attrLine)) - content.WriteString("\n") - } - } - - // Session ID if available - if i.currentLog.SessionID != "" { - sessionStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text()) - content.WriteString("\n") - content.WriteString(sessionStyle.Render("Session:")) - content.WriteString("\n") - content.WriteString(lipgloss.NewStyle().Padding(0, 2).Width(i.width).Render(i.currentLog.SessionID)) - } - - i.viewport.SetContent(content.String()) -} - -func getLevelStyle(level string) lipgloss.Style { - style := lipgloss.NewStyle().Bold(true) - t := theme.CurrentTheme() - - switch strings.ToLower(level) { - case "info": - return style.Foreground(t.Info()) - case "warn", "warning": - return style.Foreground(t.Warning()) - case "error", "err": - return style.Foreground(t.Error()) - case "debug": - return style.Foreground(t.Success()) - default: - return style.Foreground(t.Text()) - } -} - -func (i *detailCmp) View() string { - t := theme.CurrentTheme() - return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), t.Background()) -} - -func (i *detailCmp) GetSize() (int, int) { - return i.width, i.height -} - -func (i *detailCmp) SetSize(width int, height int) tea.Cmd { - i.width = width - i.height = height - i.viewport.Width = i.width - i.viewport.Height = i.height - i.updateContent() - return nil -} - -func (i *detailCmp) BindingKeys() []key.Binding { - return layout.KeyMapToSlice(i.viewport.KeyMap) -} - -func NewLogsDetails() DetailComponent { - return &detailCmp{ - viewport: viewport.New(0, 0), - } -} - -// Focus implements the focusable interface -func (i *detailCmp) Focus() { - i.focused = true - i.viewport.SetYOffset(i.viewport.YOffset) -} - -// Blur implements the blurable interface -func (i *detailCmp) Blur() { - i.focused = false -} diff --git a/internal/tui/components/logs/table.go b/internal/tui/components/logs/table.go deleted file mode 100644 index 1fc1daa38..000000000 --- a/internal/tui/components/logs/table.go +++ /dev/null @@ -1,207 +0,0 @@ -package logs - -import ( - // "context" - "fmt" - "log/slog" - - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/table" - tea "github.com/charmbracelet/bubbletea" - "github.com/sst/opencode/internal/app" - "github.com/sst/opencode/internal/logging" - "github.com/sst/opencode/internal/pubsub" - "github.com/sst/opencode/internal/tui/layout" - "github.com/sst/opencode/internal/tui/state" - "github.com/sst/opencode/internal/tui/theme" -) - -type TableComponent interface { - tea.Model - layout.Sizeable - layout.Bindings -} - -type tableCmp struct { - app *app.App - table table.Model - focused bool - logs []logging.Log - selectedLogID string -} - -type selectedLogMsg logging.Log - -type LogsLoadedMsg struct { - logs []logging.Log -} - -func (i *tableCmp) Init() tea.Cmd { - return i.fetchLogs() -} - -func (i *tableCmp) fetchLogs() tea.Cmd { - return func() tea.Msg { - // ctx := context.Background() - - var logs []logging.Log - var err error - - // Limit the number of logs to improve performance - const logLimit = 100 - // TODO: Logs service not implemented in API yet - logs = []logging.Log{} - err = fmt.Errorf("logs service not implemented") - - if err != nil { - slog.Error("Failed to fetch logs", "error", err) - return nil - } - - return LogsLoadedMsg{logs: logs} - } -} - -func (i *tableCmp) updateRows() tea.Cmd { - return func() tea.Msg { - rows := make([]table.Row, 0, len(i.logs)) - - for _, log := range i.logs { - timeStr := log.Timestamp.Local().Format("15:04:05") - - // Include ID as hidden first column for selection - row := table.Row{ - log.ID, - timeStr, - log.Level, - log.Message, - } - rows = append(rows, row) - } - - i.table.SetRows(rows) - return nil - } -} - -func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - - switch msg := msg.(type) { - case LogsLoadedMsg: - i.logs = msg.logs - return i, i.updateRows() - - case state.SessionSelectedMsg: - return i, i.fetchLogs() - - case pubsub.Event[logging.Log]: - if msg.Type == logging.EventLogCreated { - // Add the new log to our list - i.logs = append([]logging.Log{msg.Payload}, i.logs...) - // Keep the list at a reasonable size - if len(i.logs) > 100 { - i.logs = i.logs[:100] - } - return i, i.updateRows() - } - return i, nil - } - - // Only process keyboard input when focused - if _, ok := msg.(tea.KeyMsg); ok && !i.focused { - return i, nil - } - - t, cmd := i.table.Update(msg) - cmds = append(cmds, cmd) - i.table = t - - selectedRow := i.table.SelectedRow() - if selectedRow != nil { - // Only send message if it's a new selection - if i.selectedLogID != selectedRow[0] { - cmds = append(cmds, func() tea.Msg { - for _, log := range i.logs { - if log.ID == selectedRow[0] { - return selectedLogMsg(log) - } - } - return nil - }) - } - - i.selectedLogID = selectedRow[0] - } - - return i, tea.Batch(cmds...) -} - -func (i *tableCmp) View() string { - t := theme.CurrentTheme() - defaultStyles := table.DefaultStyles() - defaultStyles.Selected = defaultStyles.Selected.Foreground(t.Primary()) - i.table.SetStyles(defaultStyles) - return i.table.View() -} - -func (i *tableCmp) GetSize() (int, int) { - return i.table.Width(), i.table.Height() -} - -func (i *tableCmp) SetSize(width int, height int) tea.Cmd { - i.table.SetWidth(width) - i.table.SetHeight(height) - columns := i.table.Columns() - - // Calculate widths for visible columns - timeWidth := 8 // Fixed width for Time column - levelWidth := 7 // Fixed width for Level column - - // Message column gets the remaining space - messageWidth := width - timeWidth - levelWidth - 5 // 5 for padding and borders - - // Set column widths - columns[0].Width = 0 // ID column (hidden) - columns[1].Width = timeWidth - columns[2].Width = levelWidth - columns[3].Width = messageWidth - - i.table.SetColumns(columns) - return nil -} - -func (i *tableCmp) BindingKeys() []key.Binding { - return layout.KeyMapToSlice(i.table.KeyMap) -} - -func NewLogsTable(app *app.App) TableComponent { - columns := []table.Column{ - {Title: "ID", Width: 0}, // ID column with zero width - {Title: "Time", Width: 8}, - {Title: "Level", Width: 7}, - {Title: "Message", Width: 30}, - } - - tableModel := table.New( - table.WithColumns(columns), - ) - tableModel.Focus() - return &tableCmp{ - app: app, - table: tableModel, - logs: []logging.Log{}, - } -} - -// Focus implements the focusable interface -func (i *tableCmp) Focus() { - i.focused = true - i.table.Focus() -} - -// Blur implements the blurable interface -func (i *tableCmp) Blur() { - i.focused = false - i.table.Blur() -} |
