Kaynağa Gözat

feat: session specific logs

adamdottv 9 ay önce
ebeveyn
işleme
3982be4310

+ 24 - 3
cmd/root.go

@@ -23,6 +23,26 @@ import (
 	"github.com/sst/opencode/internal/version"
 )
 
+type SessionIDHandler struct {
+	slog.Handler
+	app *app.App
+}
+
+func (h *SessionIDHandler) Handle(ctx context.Context, r slog.Record) error {
+	if h.app != nil {
+		sessionID := h.app.CurrentSession.ID
+		if sessionID != "" {
+			r.AddAttrs(slog.String("session_id", sessionID))
+		}
+	}
+	return h.Handler.Handle(ctx, r)
+}
+
+func (h *SessionIDHandler) WithApp(app *app.App) *SessionIDHandler {
+	h.app = app
+	return h
+}
+
 var rootCmd = &cobra.Command{
 	Use:   "OpenCode",
 	Short: "A terminal AI assistant for software development",
@@ -42,9 +62,9 @@ to assist developers in writing, debugging, and understanding code directly from
 
 		// Setup logging
 		lvl := new(slog.LevelVar)
-		logger := slog.New(slog.NewTextHandler(logging.NewSlogWriter(), &slog.HandlerOptions{
-			Level: lvl,
-		}))
+		textHandler := slog.NewTextHandler(logging.NewSlogWriter(), &slog.HandlerOptions{Level: lvl})
+		sessionAwareHandler := &SessionIDHandler{Handler: textHandler}
+		logger := slog.New(sessionAwareHandler)
 		slog.SetDefault(logger)
 
 		// Load the config
@@ -89,6 +109,7 @@ to assist developers in writing, debugging, and understanding code directly from
 			slog.Error("Failed to create app", "error", err)
 			return err
 		}
+		sessionAwareHandler.WithApp(app)
 
 		// Set up the TUI
 		zone.NewGlobal()

+ 15 - 13
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

+ 1 - 1
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) {

+ 1 - 1
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

+ 0 - 9
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,

+ 2 - 9
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
 			}

+ 10 - 20
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())

+ 16 - 33
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
 	}

+ 8 - 20
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

+ 7 - 10
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):

+ 41 - 39
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()
 }

+ 19 - 23
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

+ 3 - 2
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{

+ 7 - 0
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{}

+ 24 - 12
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{})
 		},
 	})