summaryrefslogtreecommitdiffhomepage
path: root/internal
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-05-14 13:06:09 -0500
committeradamdottv <[email protected]>2025-05-14 13:06:09 -0500
commit3982be4310aa57209fd4ce2be833c3515f759ba8 (patch)
tree1bf577d7185afb153dd1f3e8fd7c4dbe36bb6448 /internal
parent4c998d4f4ff2d9570796a81a95eb84d34d0a6939 (diff)
downloadopencode-3982be4310aa57209fd4ce2be833c3515f759ba8.tar.gz
opencode-3982be4310aa57209fd4ce2be833c3515f759ba8.zip
feat: session specific logs
Diffstat (limited to 'internal')
-rw-r--r--internal/app/app.go28
-rw-r--r--internal/db/logs.sql.go2
-rw-r--r--internal/db/sql/logs.sql2
-rw-r--r--internal/tui/components/chat/chat.go9
-rw-r--r--internal/tui/components/chat/editor.go11
-rw-r--r--internal/tui/components/chat/list.go30
-rw-r--r--internal/tui/components/chat/sidebar.go49
-rw-r--r--internal/tui/components/core/status.go28
-rw-r--r--internal/tui/components/dialog/session.go17
-rw-r--r--internal/tui/components/logs/table.go80
-rw-r--r--internal/tui/page/chat.go42
-rw-r--r--internal/tui/page/logs.go5
-rw-r--r--internal/tui/state/state.go7
-rw-r--r--internal/tui/tui.go36
14 files changed, 154 insertions, 192 deletions
diff --git a/internal/app/app.go b/internal/app/app.go
index eff956c68..6c2825047 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -22,12 +22,13 @@ import (
)
type App struct {
- Logs logging.Service
- Sessions session.Service
- Messages message.Service
- History history.Service
- Permissions permission.Service
- Status status.Service
+ CurrentSession *session.Session
+ Logs logging.Service
+ Sessions session.Service
+ Messages message.Service
+ History history.Service
+ Permissions permission.Service
+ Status status.Service
PrimaryAgent agent.Service
@@ -73,13 +74,14 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
}
app := &App{
- Logs: logging.GetService(),
- Sessions: session.GetService(),
- Messages: message.GetService(),
- History: history.GetService(),
- Permissions: permission.GetService(),
- Status: status.GetService(),
- LSPClients: make(map[string]*lsp.Client),
+ CurrentSession: &session.Session{},
+ Logs: logging.GetService(),
+ Sessions: session.GetService(),
+ Messages: message.GetService(),
+ History: history.GetService(),
+ Permissions: permission.GetService(),
+ Status: status.GetService(),
+ LSPClients: make(map[string]*lsp.Client),
}
// Initialize theme based on configuration
diff --git a/internal/db/logs.sql.go b/internal/db/logs.sql.go
index 5c6ea6722..343b34d7c 100644
--- a/internal/db/logs.sql.go
+++ b/internal/db/logs.sql.go
@@ -101,7 +101,7 @@ func (q *Queries) ListAllLogs(ctx context.Context, limit int64) ([]Log, error) {
const listLogsBySession = `-- name: ListLogsBySession :many
SELECT id, session_id, timestamp, level, message, attributes, created_at, updated_at FROM logs
WHERE session_id = ?
-ORDER BY timestamp ASC
+ORDER BY timestamp DESC
`
func (q *Queries) ListLogsBySession(ctx context.Context, sessionID sql.NullString) ([]Log, error) {
diff --git a/internal/db/sql/logs.sql b/internal/db/sql/logs.sql
index 4fc640830..9d20800d9 100644
--- a/internal/db/sql/logs.sql
+++ b/internal/db/sql/logs.sql
@@ -18,7 +18,7 @@ INSERT INTO logs (
-- name: ListLogsBySession :many
SELECT * FROM logs
WHERE session_id = ?
-ORDER BY timestamp ASC;
+ORDER BY timestamp DESC;
-- name: ListAllLogs :many
SELECT * FROM logs
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()
}
diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go
index cbd4cd7b8..b9563e30c 100644
--- a/internal/tui/page/chat.go
+++ b/internal/tui/page/chat.go
@@ -13,6 +13,7 @@ import (
"github.com/sst/opencode/internal/tui/components/chat"
"github.com/sst/opencode/internal/tui/components/dialog"
"github.com/sst/opencode/internal/tui/layout"
+ "github.com/sst/opencode/internal/tui/state"
"github.com/sst/opencode/internal/tui/util"
)
@@ -23,7 +24,6 @@ type chatPage struct {
editor layout.Container
messages layout.Container
layout layout.SplitPaneLayout
- session session.Session
}
type ChatKeyMap struct {
@@ -76,16 +76,14 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if cmd != nil {
return p, cmd
}
- case chat.SessionSelectedMsg:
- if p.session.ID == "" {
- cmd := p.setSidebar()
- if cmd != nil {
- cmds = append(cmds, cmd)
- }
- }
- p.session = msg
- case chat.CompactSessionMsg:
- if p.session.ID == "" {
+ case state.SessionSelectedMsg:
+ cmd := p.setSidebar()
+ cmds = append(cmds, cmd)
+ case state.SessionClearedMsg:
+ cmd := p.setSidebar()
+ cmds = append(cmds, cmd)
+ case state.CompactSessionMsg:
+ if p.app.CurrentSession.ID == "" {
status.Warn("No active session to compact.")
return p, nil
}
@@ -98,22 +96,22 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} else {
status.Info("Conversation compacted successfully.")
}
- }(p.session.ID)
+ }(p.app.CurrentSession.ID)
return p, nil
case tea.KeyMsg:
switch {
case key.Matches(msg, keyMap.NewSession):
- p.session = session.Session{}
+ p.app.CurrentSession = &session.Session{}
return p, tea.Batch(
p.clearSidebar(),
- util.CmdHandler(chat.SessionClearedMsg{}),
+ util.CmdHandler(state.SessionClearedMsg{}),
)
case key.Matches(msg, keyMap.Cancel):
- if p.session.ID != "" {
+ if p.app.CurrentSession.ID != "" {
// Cancel the current session's generation process
// This allows users to interrupt long-running operations
- p.app.PrimaryAgent.Cancel(p.session.ID)
+ p.app.PrimaryAgent.Cancel(p.app.CurrentSession.ID)
return p, nil
}
case key.Matches(msg, keyMap.ToggleTools):
@@ -128,7 +126,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (p *chatPage) setSidebar() tea.Cmd {
sidebarContainer := layout.NewContainer(
- chat.NewSidebarCmp(p.session, p.app.History),
+ chat.NewSidebarCmp(p.app),
layout.WithPadding(1, 1, 1, 1),
)
return tea.Batch(p.layout.SetRightPanel(sidebarContainer), sidebarContainer.Init())
@@ -140,25 +138,23 @@ func (p *chatPage) clearSidebar() tea.Cmd {
func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
var cmds []tea.Cmd
- if p.session.ID == "" {
+ if p.app.CurrentSession.ID == "" {
newSession, err := p.app.Sessions.Create(context.Background(), "New Session")
if err != nil {
status.Error(err.Error())
return nil
}
- p.session = newSession
- // Update the current session in the session manager
- // session.SetCurrentSession(newSession.ID)
+ p.app.CurrentSession = &newSession
cmd := p.setSidebar()
if cmd != nil {
cmds = append(cmds, cmd)
}
- cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(newSession)))
+ cmds = append(cmds, util.CmdHandler(state.SessionSelectedMsg(&newSession)))
}
- _, err := p.app.PrimaryAgent.Run(context.Background(), p.session.ID, text, attachments...)
+ _, err := p.app.PrimaryAgent.Run(context.Background(), p.app.CurrentSession.ID, text, attachments...)
if err != nil {
status.Error(err.Error())
return nil
diff --git a/internal/tui/page/logs.go b/internal/tui/page/logs.go
index bb2624028..df5fb2c1b 100644
--- a/internal/tui/page/logs.go
+++ b/internal/tui/page/logs.go
@@ -4,6 +4,7 @@ import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/tui/components/logs"
"github.com/sst/opencode/internal/tui/layout"
"github.com/sst/opencode/internal/tui/styles"
@@ -209,9 +210,9 @@ func (p *logsPage) Init() tea.Cmd {
return tea.Batch(cmds...)
}
-func NewLogsPage() LogPage {
+func NewLogsPage(app *app.App) tea.Model {
// Create containers with borders to visually indicate active pane
- tableContainer := layout.NewContainer(logs.NewLogsTable(), layout.WithBorderHorizontal())
+ tableContainer := layout.NewContainer(logs.NewLogsTable(app), layout.WithBorderHorizontal())
detailsContainer := layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderHorizontal())
return &logsPage{
diff --git a/internal/tui/state/state.go b/internal/tui/state/state.go
new file mode 100644
index 000000000..83745d6f7
--- /dev/null
+++ b/internal/tui/state/state.go
@@ -0,0 +1,7 @@
+package state
+
+import "github.com/sst/opencode/internal/session"
+
+type SessionSelectedMsg = *session.Session
+type SessionClearedMsg struct{}
+type CompactSessionMsg struct{}
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index 9ad25c121..0bcdbdcd7 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -17,12 +17,15 @@ import (
"github.com/sst/opencode/internal/message"
"github.com/sst/opencode/internal/permission"
"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/components/core"
"github.com/sst/opencode/internal/tui/components/dialog"
+ "github.com/sst/opencode/internal/tui/components/logs"
"github.com/sst/opencode/internal/tui/layout"
"github.com/sst/opencode/internal/tui/page"
+ "github.com/sst/opencode/internal/tui/state"
"github.com/sst/opencode/internal/tui/util"
)
@@ -251,12 +254,30 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case page.PageChangeMsg:
return a, a.moveToPage(msg.ID)
+ case logs.LogsLoadedMsg:
+ a.pages[page.LogsPage], cmd = a.pages[page.LogsPage].Update(msg)
+ cmds = append(cmds, cmd)
+
+ case state.SessionSelectedMsg:
+ a.app.CurrentSession = 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
+ }
+ }
+
case dialog.CloseQuitMsg:
a.showQuit = false
return a, nil
case dialog.CloseSessionDialogMsg:
a.showSessionDialog = false
+ if msg.Session != nil {
+ return a, util.CmdHandler(state.SessionSelectedMsg(msg.Session))
+ }
return a, nil
case dialog.CloseCommandDialogMsg:
@@ -316,15 +337,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return a, nil
- case chat.SessionSelectedMsg:
- a.sessionDialog.SetSelectedSession(msg.ID)
- case dialog.SessionSelectedMsg:
- a.showSessionDialog = false
- if a.currentPage == page.ChatPage {
- return a, util.CmdHandler(chat.SessionSelectedMsg(msg.Session))
- }
- return a, nil
-
case dialog.CommandSelectedMsg:
a.showCommandDialog = false
// Execute the command handler if available
@@ -811,7 +823,7 @@ func New(app *app.App) tea.Model {
model := &appModel{
currentPage: startPage,
loadedPages: make(map[page.PageID]bool),
- status: core.NewStatusCmp(app.LSPClients),
+ status: core.NewStatusCmp(app),
help: dialog.NewHelpCmp(),
quit: dialog.NewQuitCmp(),
sessionDialog: dialog.NewSessionDialogCmp(),
@@ -824,7 +836,7 @@ func New(app *app.App) tea.Model {
commands: []dialog.Command{},
pages: map[page.PageID]tea.Model{
page.ChatPage: page.NewChatPage(app),
- page.LogsPage: page.NewLogsPage(),
+ page.LogsPage: page.NewLogsPage(app),
},
filepicker: dialog.NewFilepickerCmp(app),
}
@@ -862,7 +874,7 @@ If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (
// Return a message that will be handled by the chat page
status.Info("Compacting conversation...")
- return util.CmdHandler(chat.CompactSessionMsg{})
+ return util.CmdHandler(state.CompactSessionMsg{})
},
})