diff options
| author | adamdottv <[email protected]> | 2025-05-14 13:06:09 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-05-14 13:06:09 -0500 |
| commit | 3982be4310aa57209fd4ce2be833c3515f759ba8 (patch) | |
| tree | 1bf577d7185afb153dd1f3e8fd7c4dbe36bb6448 /internal/tui/components | |
| parent | 4c998d4f4ff2d9570796a81a95eb84d34d0a6939 (diff) | |
| download | opencode-3982be4310aa57209fd4ce2be833c3515f759ba8.tar.gz opencode-3982be4310aa57209fd4ce2be833c3515f759ba8.zip | |
feat: session specific logs
Diffstat (limited to 'internal/tui/components')
| -rw-r--r-- | internal/tui/components/chat/chat.go | 9 | ||||
| -rw-r--r-- | internal/tui/components/chat/editor.go | 11 | ||||
| -rw-r--r-- | internal/tui/components/chat/list.go | 30 | ||||
| -rw-r--r-- | internal/tui/components/chat/sidebar.go | 49 | ||||
| -rw-r--r-- | internal/tui/components/core/status.go | 28 | ||||
| -rw-r--r-- | internal/tui/components/dialog/session.go | 17 | ||||
| -rw-r--r-- | internal/tui/components/logs/table.go | 80 |
7 files changed, 84 insertions, 140 deletions
diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index 71ff10b7f..4fa6b27b2 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -8,7 +8,6 @@ import ( "github.com/charmbracelet/x/ansi" "github.com/sst/opencode/internal/config" "github.com/sst/opencode/internal/message" - "github.com/sst/opencode/internal/session" "github.com/sst/opencode/internal/tui/styles" "github.com/sst/opencode/internal/tui/theme" "github.com/sst/opencode/internal/version" @@ -19,14 +18,6 @@ type SendMsg struct { Attachments []message.Attachment } -type SessionSelectedMsg = session.Session - -type SessionClearedMsg struct{} - -type EditorFocusMsg bool - -type CompactSessionMsg struct{} - func header(width int) string { return lipgloss.JoinVertical( lipgloss.Top, diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index 9e5a72dd0..dbe55e7f9 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -13,7 +13,6 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/message" - "github.com/sst/opencode/internal/session" "github.com/sst/opencode/internal/status" "github.com/sst/opencode/internal/tui/components/dialog" "github.com/sst/opencode/internal/tui/layout" @@ -26,7 +25,6 @@ type editorCmp struct { width int height int app *app.App - session session.Session textarea textarea.Model attachments []message.Attachment deleteMode bool @@ -124,7 +122,7 @@ func (m *editorCmp) Init() tea.Cmd { } func (m *editorCmp) send() tea.Cmd { - if m.app.PrimaryAgent.IsSessionBusy(m.session.ID) { + if m.app.PrimaryAgent.IsSessionBusy(m.app.CurrentSession.ID) { status.Warn("Agent is working, please wait...") return nil } @@ -151,11 +149,6 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case dialog.ThemeChangedMsg: m.textarea = CreateTextArea(&m.textarea) return m, nil - case SessionSelectedMsg: - if msg.ID != m.session.ID { - m.session = msg - } - return m, nil case dialog.AttachmentAddedMsg: if len(m.attachments) >= maxAttachments { status.Error(fmt.Sprintf("cannot add more than %d images", maxAttachments)) @@ -189,7 +182,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.session.ID) { + if m.app.PrimaryAgent.IsSessionBusy(m.app.CurrentSession.ID) { status.Warn("Agent is working, please wait...") return m, nil } diff --git a/internal/tui/components/chat/list.go b/internal/tui/components/chat/list.go index a82dca5e0..baa7c7e6d 100644 --- a/internal/tui/components/chat/list.go +++ b/internal/tui/components/chat/list.go @@ -17,6 +17,7 @@ import ( "github.com/sst/opencode/internal/session" "github.com/sst/opencode/internal/status" "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" ) @@ -25,11 +26,11 @@ type cacheItem struct { width int content []uiMessage } + type messagesCmp struct { app *app.App width, height int viewport viewport.Model - session session.Session messages []message.Message uiMessages []uiMessage currentMsgID string @@ -84,19 +85,14 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.cachedContent = make(map[string]cacheItem) m.renderView() return m, nil - case SessionSelectedMsg: - if msg.ID != m.session.ID { - cmd := m.SetSession(msg) - return m, cmd - } - return m, nil - case SessionClearedMsg: - m.session = session.Session{} + case state.SessionSelectedMsg: + cmd := m.Reload(msg) + return m, cmd + case state.SessionClearedMsg: m.messages = make([]message.Message, 0) m.currentMsgID = "" m.rendering = false return m, nil - case tea.KeyMsg: if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) || key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) { @@ -104,15 +100,13 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewport = u cmds = append(cmds, cmd) } - case renderFinishedMsg: m.rendering = false m.viewport.GotoBottom() case pubsub.Event[message.Message]: needsRerender := false if msg.Type == message.EventMessageCreated { - if msg.Payload.SessionID == m.session.ID { - + if msg.Payload.SessionID == m.app.CurrentSession.ID { messageExists := false for _, v := range m.messages { if v.ID == msg.Payload.ID { @@ -142,7 +136,7 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } } - } else if msg.Type == message.EventMessageUpdated && msg.Payload.SessionID == m.session.ID { + } 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 @@ -170,7 +164,7 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *messagesCmp) IsAgentWorking() bool { - return m.app.PrimaryAgent.IsSessionBusy(m.session.ID) + return m.app.PrimaryAgent.IsSessionBusy(m.app.CurrentSession.ID) } func formatTimeDifference(unixTime1, unixTime2 int64) string { @@ -439,11 +433,7 @@ func (m *messagesCmp) GetSize() (int, int) { return m.width, m.height } -func (m *messagesCmp) SetSession(session session.Session) tea.Cmd { - if m.session.ID == session.ID { - return nil - } - m.session = session +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()) diff --git a/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go index f897d34cf..f2dec7878 100644 --- a/internal/tui/components/chat/sidebar.go +++ b/internal/tui/components/chat/sidebar.go @@ -8,19 +8,19 @@ 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/diff" "github.com/sst/opencode/internal/history" "github.com/sst/opencode/internal/pubsub" - "github.com/sst/opencode/internal/session" + "github.com/sst/opencode/internal/tui/state" "github.com/sst/opencode/internal/tui/styles" "github.com/sst/opencode/internal/tui/theme" ) type sidebarCmp struct { + app *app.App width, height int - session session.Session - history history.Service modFiles map[string]struct { additions int removals int @@ -28,10 +28,10 @@ type sidebarCmp struct { } func (m *sidebarCmp) Init() tea.Cmd { - if m.history != nil { + if m.app.History != nil { ctx := context.Background() // Subscribe to file events - filesCh := m.history.Subscribe(ctx) + filesCh := m.app.History.Subscribe(ctx) // Initialize the modified files map m.modFiles = make(map[string]struct { @@ -52,30 +52,14 @@ func (m *sidebarCmp) Init() tea.Cmd { func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case SessionSelectedMsg: - if msg.ID != m.session.ID { - m.session = msg - ctx := context.Background() - m.loadModifiedFiles(ctx) - } - case pubsub.Event[session.Session]: - if msg.Type == session.EventSessionUpdated { - if m.session.ID == msg.Payload.ID { - m.session = msg.Payload - } - } + case state.SessionSelectedMsg: + ctx := context.Background() + m.loadModifiedFiles(ctx) case pubsub.Event[history.File]: - if msg.Payload.SessionID == m.session.ID { + if msg.Payload.SessionID == m.app.CurrentSession.ID { // Process the individual file change instead of reloading all files ctx := context.Background() m.processFileChanges(ctx, msg.Payload) - - // Return a command to continue receiving events - return m, func() tea.Msg { - ctx := context.Background() - filesCh := m.history.Subscribe(ctx) - return <-filesCh - } } } return m, nil @@ -115,7 +99,7 @@ func (m *sidebarCmp) sessionSection() string { sessionValue := baseStyle. Foreground(t.Text()). Width(m.width - lipgloss.Width(sessionKey)). - Render(fmt.Sprintf(": %s", m.session.Title)) + Render(fmt.Sprintf(": %s", m.app.CurrentSession.Title)) return lipgloss.JoinHorizontal( lipgloss.Left, @@ -235,26 +219,25 @@ func (m *sidebarCmp) GetSize() (int, int) { return m.width, m.height } -func NewSidebarCmp(session session.Session, history history.Service) tea.Model { +func NewSidebarCmp(app *app.App) tea.Model { return &sidebarCmp{ - session: session, - history: history, + app: app, } } func (m *sidebarCmp) loadModifiedFiles(ctx context.Context) { - if m.history == nil || m.session.ID == "" { + if m.app.CurrentSession.ID == "" { return } // Get all latest files for this session - latestFiles, err := m.history.ListLatestSessionFiles(ctx, m.session.ID) + 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.history.ListBySession(ctx, m.session.ID) + allFiles, err := m.app.History.ListBySession(ctx, m.app.CurrentSession.ID) if err != nil { return } @@ -355,7 +338,7 @@ func (m *sidebarCmp) processFileChanges(ctx context.Context, file history.File) // Helper function to find the initial version of a file func (m *sidebarCmp) findInitialVersion(ctx context.Context, path string) (history.File, error) { // Get all versions of this file for the session - fileVersions, err := m.history.ListBySession(ctx, m.session.ID) + fileVersions, err := m.app.History.ListBySession(ctx, m.app.CurrentSession.ID) if err != nil { return history.File{}, err } diff --git a/internal/tui/components/core/status.go b/internal/tui/components/core/status.go index 7985ee007..5a24a45af 100644 --- a/internal/tui/components/core/status.go +++ b/internal/tui/components/core/status.go @@ -7,14 +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/session" "github.com/sst/opencode/internal/status" - "github.com/sst/opencode/internal/tui/components/chat" "github.com/sst/opencode/internal/tui/styles" "github.com/sst/opencode/internal/tui/theme" ) @@ -25,11 +24,10 @@ type StatusCmp interface { } type statusCmp struct { + app *app.App statusMessages []statusMessage width int messageTTL time.Duration - lspClients map[string]*lsp.Client - session session.Session } type statusMessage struct { @@ -60,16 +58,6 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.width = msg.Width return m, nil - case chat.SessionSelectedMsg: - m.session = msg - case chat.SessionClearedMsg: - m.session = session.Session{} - case pubsub.Event[session.Session]: - if msg.Type == session.EventSessionUpdated { - if m.session.ID == msg.Payload.ID { - m.session = msg.Payload - } - } case pubsub.Event[status.StatusMessage]: if msg.Type == status.EventStatusPublished { statusMsg := statusMessage{ @@ -146,8 +134,8 @@ func (m statusCmp) View() string { // Initialize the help widget status := getHelpWidget("") - if m.session.ID != "" { - tokens := formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, model.ContextWindow, m.session.Cost) + if m.app.CurrentSession.ID != "" { + tokens := formatTokensAndCost(m.app.CurrentSession.PromptTokens+m.app.CurrentSession.CompletionTokens, model.ContextWindow, m.app.CurrentSession.Cost) tokensStyle := styles.Padded(). Background(t.Text()). Foreground(t.BackgroundSecondary()). @@ -211,7 +199,7 @@ func (m *statusCmp) projectDiagnostics() string { // Check if any LSP server is still initializing initializing := false - for _, client := range m.lspClients { + for _, client := range m.app.LSPClients { if client.GetServerState() == lsp.StateStarting { initializing = true break @@ -229,7 +217,7 @@ func (m *statusCmp) projectDiagnostics() string { warnDiagnostics := []protocol.Diagnostic{} hintDiagnostics := []protocol.Diagnostic{} infoDiagnostics := []protocol.Diagnostic{} - for _, client := range m.lspClients { + for _, client := range m.app.LSPClients { for _, d := range client.GetDiagnostics() { for _, diag := range d { switch diag.Severity { @@ -300,14 +288,14 @@ func (m statusCmp) SetHelpWidgetMsg(s string) { helpWidget = getHelpWidget(s) } -func NewStatusCmp(lspClients map[string]*lsp.Client) StatusCmp { +func NewStatusCmp(app *app.App) StatusCmp { // Initialize the help widget with default text helpWidget = getHelpWidget("") statusComponent := &statusCmp{ + app: app, statusMessages: []statusMessage{}, messageTTL: 4 * time.Second, - lspClients: lspClients, } return statusComponent diff --git a/internal/tui/components/dialog/session.go b/internal/tui/components/dialog/session.go index 1d2ba6a19..0ec828266 100644 --- a/internal/tui/components/dialog/session.go +++ b/internal/tui/components/dialog/session.go @@ -11,13 +11,10 @@ import ( "github.com/sst/opencode/internal/tui/util" ) -// SessionSelectedMsg is sent when a session is selected -type SessionSelectedMsg struct { - Session session.Session -} - // CloseSessionDialogMsg is sent when the session dialog is closed -type CloseSessionDialogMsg struct{} +type CloseSessionDialogMsg struct { + Session *session.Session +} // SessionDialog interface for the session switching dialog type SessionDialog interface { @@ -92,10 +89,10 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, sessionKeys.Enter): if len(s.sessions) > 0 { selectedSession := s.sessions[s.selectedIdx] - // Update the session manager with the selected session - // session.SetCurrentSession(selectedSession.ID) - return s, util.CmdHandler(SessionSelectedMsg{ - Session: selectedSession, + s.selectedSessionID = selectedSession.ID + + return s, util.CmdHandler(CloseSessionDialogMsg{ + Session: &selectedSession, }) } case key.Matches(msg, sessionKeys.Escape): diff --git a/internal/tui/components/logs/table.go b/internal/tui/components/logs/table.go index fa701adee..4d33679bb 100644 --- a/internal/tui/components/logs/table.go +++ b/internal/tui/components/logs/table.go @@ -2,14 +2,16 @@ package logs import ( "context" + "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/components/chat" "github.com/sst/opencode/internal/tui/layout" + "github.com/sst/opencode/internal/tui/state" "github.com/sst/opencode/internal/tui/theme" ) @@ -20,6 +22,7 @@ type TableComponent interface { } type tableCmp struct { + app *app.App table table.Model focused bool logs []logging.Log @@ -28,7 +31,7 @@ type tableCmp struct { type selectedLogMsg logging.Log -type logsLoadedMsg struct { +type LogsLoadedMsg struct { logs []logging.Log } @@ -39,21 +42,17 @@ func (i *tableCmp) Init() tea.Cmd { func (i *tableCmp) fetchLogs() tea.Cmd { return func() tea.Msg { ctx := context.Background() - loggingService := logging.GetService() - if loggingService == nil { - return nil - } var logs []logging.Log var err error - sessionId := "" // TODO: session.CurrentSessionID() // Limit the number of logs to improve performance const logLimit = 100 - if sessionId == "" { - logs, err = loggingService.ListAll(ctx, logLimit) + if i.app.CurrentSession.ID == "" { + logs, err = i.app.Logs.ListAll(ctx, logLimit) } else { - logs, err = loggingService.ListBySession(ctx, sessionId) + logs, err = i.app.Logs.ListBySession(ctx, i.app.CurrentSession.ID) + // Trim logs if there are too many if err == nil && len(logs) > logLimit { logs = logs[len(logs)-logLimit:] @@ -61,10 +60,33 @@ func (i *tableCmp) fetchLogs() tea.Cmd { } if err != nil { + slog.Error("Failed to fetch logs", "error", err) return nil } - return logsLoadedMsg{logs: logs} + 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 } } @@ -72,12 +94,11 @@ func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { - case logsLoadedMsg: + case LogsLoadedMsg: i.logs = msg.logs - i.updateRows() - return i, nil + return i, i.updateRows() - case chat.SessionSelectedMsg: + case state.SessionSelectedMsg: return i, i.fetchLogs() case pubsub.Event[logging.Log]: @@ -88,7 +109,7 @@ func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if len(i.logs) > 100 { i.logs = i.logs[:100] } - i.updateRows() + return i, i.updateRows() } return i, nil } @@ -116,9 +137,9 @@ func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { }) } + i.selectedLogID = selectedRow[0] } - i.selectedLogID = selectedRow[0] return i, tea.Batch(cmds...) } @@ -160,26 +181,7 @@ func (i *tableCmp) BindingKeys() []key.Binding { return layout.KeyMapToSlice(i.table.KeyMap) } -func (i *tableCmp) updateRows() { - 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) -} - -func NewLogsTable() TableComponent { +func NewLogsTable(app *app.App) TableComponent { columns := []table.Column{ {Title: "ID", Width: 0}, // ID column with zero width {Title: "Time", Width: 8}, @@ -192,6 +194,7 @@ func NewLogsTable() TableComponent { ) tableModel.Focus() return &tableCmp{ + app: app, table: tableModel, logs: []logging.Log{}, } @@ -206,6 +209,5 @@ func (i *tableCmp) Focus() { // Blur implements the blurable interface func (i *tableCmp) Blur() { i.focused = false - // Table doesn't have a Blur method, but we can implement it here - // to satisfy the interface + i.table.Blur() } |
