Skip to main content
Glama
errors_view_controller.go14.4 kB
package tui import ( "fmt" "regexp" "strings" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/standardbeagle/brummer/internal/logs" ) // errorUpdateMsg represents an error update event type errorUpdateMsg struct{} // errorItem implements list.Item for error contexts type errorItem struct { errorContext *logs.ErrorContext timestamp string processName string } func (i errorItem) FilterValue() string { return i.errorContext.Message } func (i errorItem) Title() string { return fmt.Sprintf("%s [%s] %s", i.timestamp, i.processName, i.errorContext.Type) } func (i errorItem) Description() string { message := i.errorContext.Message if len(message) > 100 { message = message[:97] + "..." } return message } // ErrorsViewController manages the errors view state and rendering type ErrorsViewController struct { errorsViewport viewport.Model errorDetailView viewport.Model errorsList list.Model selectedError *logs.ErrorContext lastErrorCount int // Dependencies injected from parent Model logStore *logs.Store width int height int headerHeight int footerHeight int } // NewErrorsViewController creates a new errors view controller func NewErrorsViewController(logStore *logs.Store) *ErrorsViewController { // Initialize errors list errorsList := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) errorsList.Title = "Errors" errorsList.SetShowStatusBar(false) return &ErrorsViewController{ errorsViewport: viewport.New(0, 0), errorDetailView: viewport.New(0, 0), errorsList: errorsList, logStore: logStore, } } // UpdateSize updates the viewport dimensions with pre-calculated content height func (v *ErrorsViewController) UpdateSize(width, height, headerHeight, footerHeight, contentHeight int) { v.width = width v.height = height v.headerHeight = headerHeight v.footerHeight = footerHeight v.errorsViewport.Width = width v.errorsViewport.Height = contentHeight // Update other components v.errorDetailView.Width = width v.errorDetailView.Height = contentHeight v.errorsList.SetSize(width, contentHeight) } // GetErrorsViewport returns the errors viewport for direct manipulation func (v *ErrorsViewController) GetErrorsViewport() *viewport.Model { return &v.errorsViewport } // UpdateErrorsView refreshes the errors view with current data func (v *ErrorsViewController) UpdateErrorsView() { errorContexts := v.logStore.GetErrorContexts() var content strings.Builder content.WriteString(lipgloss.NewStyle().Bold(true).Render("Recent Errors") + "\n\n") if len(errorContexts) == 0 { // Fall back to simple errors if no contexts errors := v.logStore.GetErrors() if len(errors) == 0 { content.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("245")).Render("No errors detected yet. Use /clear errors to clear when errors appear")) } else { errorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("196")) timeStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("245")) processStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("214")) // Show most recent errors first for i := len(errors) - 1; i >= 0 && i >= len(errors)-20; i-- { err := errors[i] content.WriteString(fmt.Sprintf("%s %s\n%s\n\n", timeStyle.Render(err.Timestamp.Format("15:04:05")), processStyle.Render(fmt.Sprintf("[%s]", err.ProcessName)), errorStyle.Render(err.Content), )) } } } else { // Styles for different parts errorTypeStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true) timeStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("245")) processStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("214")) messageStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("196")) stackStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) contextStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("243")) separatorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("236")) // Show most recent error contexts first shown := 0 for i := len(errorContexts) - 1; i >= 0 && shown < 10; i-- { errorCtx := errorContexts[i] // Error header content.WriteString(fmt.Sprintf("%s %s %s\n", timeStyle.Render(errorCtx.Timestamp.Format("15:04:05")), processStyle.Render(fmt.Sprintf("[%s]", errorCtx.ProcessName)), errorTypeStyle.Render(errorCtx.Type), )) // Main error message content.WriteString(messageStyle.Render(errorCtx.Message) + "\n") // Stack trace if available if len(errorCtx.Stack) > 0 { content.WriteString(stackStyle.Render("Stack Trace:") + "\n") for j, stackLine := range errorCtx.Stack { if j > 5 { // Limit stack trace lines content.WriteString(stackStyle.Render(fmt.Sprintf(" ... and %d more lines", len(errorCtx.Stack)-j)) + "\n") break } content.WriteString(stackStyle.Render(" "+strings.TrimSpace(stackLine)) + "\n") } } // Additional context if available if len(errorCtx.Context) > 0 && len(errorCtx.Context) <= 5 { for _, ctxLine := range errorCtx.Context { if strings.TrimSpace(ctxLine) != "" { content.WriteString(contextStyle.Render(" "+strings.TrimSpace(ctxLine)) + "\n") } } } // Separator between errors content.WriteString(separatorStyle.Render("─────────────────────────────────────────") + "\n\n") shown++ } } v.errorsViewport.SetContent(content.String()) } // Render renders the errors view func (v *ErrorsViewController) Render() string { // Update content with latest errors v.UpdateErrorsView() return v.errorsViewport.View() } // GetErrorsList returns the errors list for direct manipulation func (v *ErrorsViewController) GetErrorsList() *list.Model { return &v.errorsList } // GetErrorDetailView returns the error detail viewport for direct manipulation func (v *ErrorsViewController) GetErrorDetailView() *viewport.Model { return &v.errorDetailView } // SetSelectedError sets the currently selected error func (v *ErrorsViewController) SetSelectedError(errorCtx *logs.ErrorContext) { v.selectedError = errorCtx } // GetSelectedError returns the currently selected error func (v *ErrorsViewController) GetSelectedError() *logs.ErrorContext { return v.selectedError } // UpdateErrorsList refreshes the errors list with current data func (v *ErrorsViewController) UpdateErrorsList() int { errorContexts := v.logStore.GetErrorContexts() newCount := len(errorContexts) items := make([]list.Item, 0, len(errorContexts)) for i := len(errorContexts) - 1; i >= 0; i-- { errorCtx := errorContexts[i] items = append(items, errorItem{ errorContext: &errorCtx, timestamp: errorCtx.Timestamp.Format("15:04:05"), processName: errorCtx.ProcessName, }) } v.errorsList.SetItems(items) // Return count change for unread indicator updates countChange := newCount - v.lastErrorCount v.lastErrorCount = newCount return countChange } // UpdateErrorDetailView updates the error detail view with the selected error func (v *ErrorsViewController) UpdateErrorDetailView() { if v.selectedError == nil { v.errorDetailView.SetContent("Select an error to view details") return } var content strings.Builder // Error header with enhanced styling headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205")) timeStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("245")) processStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("214")) content.WriteString(fmt.Sprintf("%s %s %s\n\n", timeStyle.Render(v.selectedError.Timestamp.Format("2006-01-02 15:04:05")), processStyle.Render(fmt.Sprintf("[%s]", v.selectedError.ProcessName)), headerStyle.Render(v.selectedError.Type), )) // Main error message messageStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("196")) content.WriteString(messageStyle.Render("Error Message:") + "\n") content.WriteString(v.selectedError.Message + "\n\n") // File reference if available fileRef := v.findLowestCodeReference(v.selectedError) if fileRef != "" { refStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true) content.WriteString(refStyle.Render("📍 Code Reference: ") + fileRef + "\n\n") } // Stack trace if len(v.selectedError.Stack) > 0 { stackStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) content.WriteString(stackStyle.Render("Stack Trace:") + "\n") for i, stackLine := range v.selectedError.Stack { if i > 10 { // Show more lines in detail view content.WriteString(stackStyle.Render(fmt.Sprintf(" ... and %d more lines", len(v.selectedError.Stack)-i)) + "\n") break } content.WriteString(stackStyle.Render(" "+strings.TrimSpace(stackLine)) + "\n") } content.WriteString("\n") } // Additional context if len(v.selectedError.Context) > 0 { contextStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("243")) content.WriteString(contextStyle.Render("Additional Context:") + "\n") for _, ctxLine := range v.selectedError.Context { if strings.TrimSpace(ctxLine) != "" { content.WriteString(contextStyle.Render(" "+strings.TrimSpace(ctxLine)) + "\n") } } } v.errorDetailView.SetContent(content.String()) } // HandleClearErrors clears all errors and logs the action func (v *ErrorsViewController) HandleClearErrors() { v.logStore.ClearErrors() v.logStore.Add("system", "System", "🗑️ Error history cleared", false) v.selectedError = nil v.lastErrorCount = 0 } // HandleCopyError creates a command to copy error details to clipboard func (v *ErrorsViewController) HandleCopyError() tea.Cmd { return func() tea.Msg { // Try to get error contexts first errorContexts := v.logStore.GetErrorContexts() var errorText string if len(errorContexts) > 0 { // Use the most recent error context errorCtx := errorContexts[len(errorContexts)-1] var builder strings.Builder builder.WriteString(fmt.Sprintf("Error: %s\n", errorCtx.Type)) builder.WriteString(fmt.Sprintf("Process: %s\n", errorCtx.ProcessName)) builder.WriteString(fmt.Sprintf("Time: %s\n", errorCtx.Timestamp.Format("2006-01-02 15:04:05"))) builder.WriteString(fmt.Sprintf("Message: %s\n", errorCtx.Message)) if len(errorCtx.Stack) > 0 { builder.WriteString("\nStack Trace:\n") for _, line := range errorCtx.Stack { builder.WriteString(fmt.Sprintf(" %s\n", strings.TrimSpace(line))) } } if len(errorCtx.Context) > 0 { builder.WriteString("\nContext:\n") for _, line := range errorCtx.Context { if strings.TrimSpace(line) != "" { builder.WriteString(fmt.Sprintf(" %s\n", strings.TrimSpace(line))) } } } errorText = builder.String() } else { // Fall back to simple error format errors := v.logStore.GetErrors() if len(errors) > 0 { latestError := errors[len(errors)-1] errorText = fmt.Sprintf("Error from %s at %s:\n%s", latestError.ProcessName, latestError.Timestamp.Format("2006-01-02 15:04:05"), latestError.Content, ) } else { errorText = "No errors to copy" } } // TODO: Implement actual clipboard copy functionality // For now, just return a message that text would be copied _ = errorText // Will be used for actual clipboard implementation return struct { message string }{ message: "Error details copied to clipboard", } } } // RenderErrorsViewSplit renders a split view with error list and details func (v *ErrorsViewController) RenderErrorsViewSplit() string { if v.width < 100 { // For narrow screens, use the old view return v.Render() } // Update both views v.UpdateErrorsList() v.UpdateErrorDetailView() // Use ContentLayout for split view if available splitRatio := DefaultSplitRatio leftWidth := int(float64(v.width) * splitRatio) rightWidth := v.width - leftWidth // Update list size for split view v.errorsList.SetSize(leftWidth-2, v.height-v.headerHeight-v.footerHeight) v.errorDetailView.Width = rightWidth - 2 v.errorDetailView.Height = v.height - v.headerHeight - v.footerHeight // Create left panel with border leftPanel := lipgloss.NewStyle(). Width(leftWidth). Height(v.height-v.headerHeight-v.footerHeight). Border(lipgloss.NormalBorder(), false, true, false, false). BorderForeground(lipgloss.Color("240")). Render(v.errorsList.View()) // Create right panel rightPanel := lipgloss.NewStyle(). Width(rightWidth). Height(v.height - v.headerHeight - v.footerHeight). Render(v.errorDetailView.View()) // Join horizontally return lipgloss.JoinHorizontal(lipgloss.Top, leftPanel, rightPanel) } // findLowestCodeReference finds the lowest-level code reference in error context func (v *ErrorsViewController) findLowestCodeReference(errorCtx *logs.ErrorContext) string { // Look for file paths with line numbers in stack traces filePattern := regexp.MustCompile(`([^\s\(\)]+\.(js|ts|jsx|tsx|go|py|java|rs|rb|php)):(\d+)(?::(\d+))?`) var lowestRef string var lowestInProject bool // Check stack trace first for _, line := range errorCtx.Stack { matches := filePattern.FindStringSubmatch(line) if len(matches) >= 4 { filepath := matches[1] lineNum := matches[3] // Prefer files that look like they're in the project (not node_modules, etc.) isInProject := !strings.Contains(filepath, "node_modules") && !strings.Contains(filepath, ".pnpm") && !strings.Contains(filepath, "/usr/") && !strings.Contains(filepath, "/opt/") // If we don't have a reference yet, or this one is better if lowestRef == "" || (isInProject && !lowestInProject) { if len(matches) >= 5 && matches[4] != "" { lowestRef = fmt.Sprintf("%s:%s:%s", filepath, lineNum, matches[4]) } else { lowestRef = fmt.Sprintf("%s:%s", filepath, lineNum) } lowestInProject = isInProject } } } // Also check the main error message if lowestRef == "" { matches := filePattern.FindStringSubmatch(errorCtx.Message) if len(matches) >= 4 { filepath := matches[1] lineNum := matches[3] if len(matches) >= 5 && matches[4] != "" { lowestRef = fmt.Sprintf("%s:%s:%s", filepath, lineNum, matches[4]) } else { lowestRef = fmt.Sprintf("%s:%s", filepath, lineNum) } } } return lowestRef }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/standardbeagle/brummer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server