diff --git a/.github/prompts/compile.prompt.md b/.github/prompts/compile.prompt.md index 944dd3c..e375ee3 100644 --- a/.github/prompts/compile.prompt.md +++ b/.github/prompts/compile.prompt.md @@ -3,5 +3,5 @@ mode: agent --- - Update the app to follow [the specification](../../main.md) -- Build the code with the VS Code tasks. Avoid asking me to run `go build` or `go test` commands manually. +- Build and run the code with `scripts/run`. Avoid asking me to run `go build` or `go test` commands manually. - Fetch the GitHub home page for each used library to get a documentation and examples. Avoid the weird go code inspections you like to do. diff --git a/.github/skills/testing/SKILL.md b/.github/skills/testing/SKILL.md index a8400d9..89087ad 100644 --- a/.github/skills/testing/SKILL.md +++ b/.github/skills/testing/SKILL.md @@ -1,6 +1,6 @@ --- name: testing -description: Guide for testing TUI (terminal user interface) applications. Use this when asked to verify code changes. +description: Guide for testing and verifying code changes in this TUI application. Use this skill after making ANY code changes to main.go or main.md to verify they work correctly. --- # Testing @@ -9,17 +9,20 @@ This skill helps you create and run tests for terminal user interface (TUI) appl ## When to use this skill -Use this skill when you need to: -- Verify that code changes work as intended -- Ensure existing functionality as specified in `maind.md` and `README.md` is not broken +Use this skill: + +- **After making ANY code changes** to verify they work correctly +- When modifying UI elements, menus, or display logic +- When changing application behavior or adding features +- To ensure existing functionality as specified in `main.md` and `README.md` is not broken ## Starting the application - Run `scripts/run` where the user would normally run `github-brain` - `scripts/run pull` equivalently runs `github-brain pull` - - `scripts/run mcp` equivalently runs `github-brain mcp` + - `scripts/run mcp` equivalently runs `github-brain mcp` - Ensure `.env` files is configured to use the `github-brain-test` organization - Use GitHub MCP to add new issue/PRs/discussions as needed for testing - Simulate user input: Send key presses, control combinations, or specific commands to the running application. -- Capture and analyze screen output: The testing tool captures the terminal display (or buffer) at specific moments in time. +- Capture and analyze screen output: The testing tool captures the terminal display (or buffer) at specific moments in time. - Make assertions: Verify that the screen content matches the expected output (e.g., checking if specific text is present at certain coordinates or if the cursor position is correct). diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index a4b76f0..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Build github-brain", - "type": "shell", - "command": "cd /Users/wham/code/github-brain && CGO_CFLAGS=\"-DSQLITE_ENABLE_FTS5\" go build -o build/github-brain .", - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": ["$go"] - } - ] -} diff --git a/main.go b/main.go index 79ece0a..17b7fe5 100644 --- a/main.go +++ b/main.go @@ -62,14 +62,40 @@ var ( statusMutex sync.Mutex ) -// gradientColors defines the color gradient for UI borders (purple → blue → cyan) -var gradientColors = []lipgloss.AdaptiveColor{ - {Light: "#874BFD", Dark: "#7D56F4"}, // Purple - {Light: "#7D56F4", Dark: "#6B4FD8"}, // Purple-blue - {Light: "#5B4FE0", Dark: "#5948C8"}, // Blue-purple - {Light: "#4F7BD8", Dark: "#4B6FD0"}, // Blue - {Light: "#48A8D8", Dark: "#45A0D0"}, // Cyan-blue - {Light: "#48D8D0", Dark: "#45D0C8"}, // Cyan +// borderColor defines the static purple color for UI borders +var borderColor = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} + +// Common UI styles - defined once, used throughout +var ( + titleStyle = lipgloss.NewStyle().Bold(true) + dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) + successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) // Bright green + errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) // Bright red + activeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12")) // Bright blue +) + +// renderTitleBar renders a title bar with left title and right-aligned user status +func renderTitleBar(screen, username, organization string, innerWidth int) string { + leftTitle := fmt.Sprintf("GitHub Brain %s / %s", Version, screen) + var rightStatus string + if username != "" { + if organization != "" { + rightStatus = fmt.Sprintf("👤 @%s (%s)", username, organization) + } else { + rightStatus = fmt.Sprintf("👤 @%s (no org)", username) + } + } else { + rightStatus = "👤 Not logged in" + } + + leftWidth := lipgloss.Width(leftTitle) + rightWidth := lipgloss.Width(rightStatus) + spacing := innerWidth - leftWidth - rightWidth + if spacing < 1 { + spacing = 1 + } + + return titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) } // Removed ConsoleHandler - not needed with Bubble Tea @@ -4355,7 +4381,7 @@ type ProgressInterface interface { Start() Stop() StopWithPreserve() - InitItems(config *Config) + InitItems(config *Config, username string) UpdateItemCount(item string, count int) MarkItemCompleted(item string, count int) MarkItemFailed(item string, message string) @@ -4388,13 +4414,13 @@ func (p *UIProgress) Start() { } // InitItems initializes the items to display based on config -func (p *UIProgress) InitItems(config *Config) { +func (p *UIProgress) InitItems(config *Config, username string) { enabledItems := make(map[string]bool) for _, item := range config.Items { enabledItems[item] = true } - m := newModel(enabledItems) + m := newModel(enabledItems, username, config.Organization) // Use WithAltScreen to run in alternate screen mode (prevents multiple boxes) p.program = tea.NewProgram(m, tea.WithAltScreen()) @@ -4492,8 +4518,7 @@ func (p *UIProgress) UpdateRequestRate(requestsPerSecond int) { // Message types for Bubble Tea updates type ( - tickMsg time.Time - itemUpdateMsg struct { + itemUpdateMsg struct { item string count int } @@ -4543,8 +4568,8 @@ type model struct { rateLimitReset time.Time width int height int - borderColors []lipgloss.AdaptiveColor - colorIndex int + username string + organization string } // logEntry represents a timestamped log message (renamed from LogEntry to avoid conflict) @@ -4554,7 +4579,7 @@ type logEntry struct { } // newModel creates a new Bubble Tea model -func newModel(enabledItems map[string]bool) model { +func newModel(enabledItems map[string]bool, username, organization string) model { s := spinner.New() s.Spinner = spinner.Dot s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("12")) // Bright blue @@ -4578,25 +4603,15 @@ func newModel(enabledItems map[string]bool) model { spinner: s, logs: make([]logEntry, 0, 5), width: 80, - height: 24, - borderColors: gradientColors, - colorIndex: 0, + username: username, + organization: organization, + height: 24, } } // Init initializes the Bubble Tea model func (m model) Init() tea.Cmd { - return tea.Batch( - m.spinner.Tick, - tickCmd(), - ) -} - -// tickCmd returns a command that ticks every second for border animation -func tickCmd() tea.Cmd { - return tea.Tick(time.Second, func(t time.Time) tea.Msg { - return tickMsg(t) - }) + return m.spinner.Tick } // Update handles messages and updates the model @@ -4606,19 +4621,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { - case "ctrl+c", "q": + case "ctrl+c": return m, tea.Quit } case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height - return m, nil - - case tickMsg: - // Rotate border color - m.colorIndex = (m.colorIndex + 1) % len(m.borderColors) - return m, tickCmd() + return m, tea.ClearScreen case itemUpdateMsg: if state, exists := m.items[msg.item]; exists { @@ -4705,12 +4715,7 @@ func (m *model) addLog(message string) { // View renders the UI func (m model) View() string { - // Define colors and styles - borderColor := m.borderColors[m.colorIndex] - dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) - activeStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")) // Bright blue - completeStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")) // Bright green - errorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("9")) // Bright red + // Local style for header (not commonly reused) headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("7")) // White // Build content lines @@ -4722,14 +4727,14 @@ func (m model) View() string { // Items section for _, name := range m.itemOrder { state := m.items[name] - lines = append(lines, formatItemLine(state, m.spinner.View(), dimStyle, activeStyle, completeStyle, errorStyle)) + lines = append(lines, formatItemLine(state, m.spinner.View(), dimStyle, activeStyle, successStyle, errorStyle)) } // Empty line lines = append(lines, "") // API Status line - lines = append(lines, formatAPIStatusLine(m.apiSuccess, m.apiWarning, m.apiErrors, headerStyle, completeStyle, errorStyle)) + lines = append(lines, formatAPIStatusLine(m.apiSuccess, m.apiWarning, m.apiErrors, headerStyle, successStyle, errorStyle)) // Rate Limit line lines = append(lines, formatRateLimitLine(m.rateLimitUsed, m.rateLimitMax, m.rateLimitReset, headerStyle)) @@ -4809,13 +4814,27 @@ func (m model) View() string { } // Add title as first line of content - titleStyle := lipgloss.NewStyle().Bold(true) - titleLine := titleStyle.Render("GitHub 🧠 Pull") - // Pad title line to match content width - titlePadding := maxContentWidth - visibleLength(titleLine) - if titlePadding > 0 { - titleLine = titleLine + strings.Repeat(" ", titlePadding) + leftTitle := fmt.Sprintf("GitHub Brain %s / 📥 Pull", Version) + var rightStatus string + if m.username != "" { + if m.organization != "" { + rightStatus = fmt.Sprintf("👤 @%s (%s)", m.username, m.organization) + } else { + rightStatus = fmt.Sprintf("👤 @%s (no org)", m.username) + } + } else { + rightStatus = "👤 Not logged in" + } + + // Calculate spacing for title bar + leftWidth := visibleLength(leftTitle) + rightWidth := visibleLength(rightStatus) + spacing := maxContentWidth - leftWidth - rightWidth + if spacing < 1 { + spacing = 1 } + + titleLine := titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) contentLines = append([]string{titleLine}, contentLines...) content = strings.Join(contentLines, "\n") @@ -4833,7 +4852,7 @@ func (m model) View() string { // Helper formatting functions (return plain strings, box handles borders) -func formatItemLine(state itemState, spinnerView string, dimStyle, activeStyle, completeStyle, errorStyle lipgloss.Style) string { +func formatItemLine(state itemState, spinnerView string, dimStyle, activeStyle, successStyle, errorStyle lipgloss.Style) string { var icon string var style lipgloss.Style var text string @@ -4850,7 +4869,7 @@ func formatItemLine(state itemState, spinnerView string, dimStyle, activeStyle, } } else if state.completed { icon = "✅" - style = completeStyle + style = successStyle text = fmt.Sprintf("%s: %s", displayName, formatNumber(state.count)) } else if state.active { icon = spinnerView @@ -4873,7 +4892,7 @@ func formatItemLine(state itemState, spinnerView string, dimStyle, activeStyle, return style.Render(icon + " " + text) } -func formatAPIStatusLine(success, warning, errors int, headerStyle, completeStyle, errorStyle lipgloss.Style) string { +func formatAPIStatusLine(success, warning, errors int, headerStyle, successStyle, errorStyle lipgloss.Style) string { // Match the pattern of formatRateLimitLine - only style the header // Note: Using 🟡 instead of ⚠️ because the warning sign has a variation selector that breaks width calculation apiText := fmt.Sprintf("✅ %s 🟡 %s ❌ %s ", @@ -4918,8 +4937,6 @@ type mainMenuModel struct { organization string width int height int - borderColors []lipgloss.AdaptiveColor - colorIndex int quitting bool runSetup bool runPull bool @@ -4927,49 +4944,36 @@ type mainMenuModel struct { } type menuChoice struct { + icon string name string description string } // Message types for main menu -type ( - mainMenuTickMsg time.Time - authCheckResultMsg struct { - loggedIn bool - username string - organization string - } -) +type authCheckResultMsg struct { + loggedIn bool + username string + organization string +} func newMainMenuModel(homeDir string) mainMenuModel { return mainMenuModel{ homeDir: homeDir, choices: []menuChoice{ - {name: "Setup", description: "Configure authentication and settings"}, - {name: "Pull", description: "Sync GitHub data to local database"}, - {name: "Quit", description: "Exit"}, + {icon: "🔧", name: "Setup", description: "Configure authentication and settings"}, + {icon: "📥", name: "Pull", description: "Sync GitHub data to local database"}, + {icon: "🚪", name: "Quit", description: "Exit"}, }, cursor: 0, status: "Checking authentication...", width: 80, height: 24, - borderColors: gradientColors, - colorIndex: 0, checkingAuth: true, } } func (m mainMenuModel) Init() tea.Cmd { - return tea.Batch( - mainMenuTickCmd(), - checkAuthCmd(m.homeDir), - ) -} - -func mainMenuTickCmd() tea.Cmd { - return tea.Tick(time.Second, func(t time.Time) tea.Msg { - return mainMenuTickMsg(t) - }) + return checkAuthCmd(m.homeDir) } func checkAuthCmd(homeDir string) tea.Cmd { @@ -5008,7 +5012,7 @@ func (m mainMenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { - case "ctrl+c", "q": + case "ctrl+c": m.quitting = true return m, tea.Quit case "up", "k": @@ -5036,11 +5040,7 @@ func (m mainMenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height - return m, nil - - case mainMenuTickMsg: - m.colorIndex = (m.colorIndex + 1) % len(m.borderColors) - return m, mainMenuTickCmd() + return m, tea.ClearScreen case authCheckResultMsg: m.checkingAuth = false @@ -5068,15 +5068,19 @@ func (m mainMenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m mainMenuModel) View() string { - borderColor := m.borderColors[m.colorIndex] - var b strings.Builder - titleStyle := lipgloss.NewStyle().Bold(true) - dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")).Bold(true) - b.WriteString(titleStyle.Render(" GitHub 🧠") + "\n") + // Calculate box width for title bar + boxContentWidth := m.width - 2 + if boxContentWidth < 60 { + boxContentWidth = 60 + } + // Inner width is box content width minus padding (1 on each side) + innerWidth := boxContentWidth - 2 + + b.WriteString(renderTitleBar("🏠 Home", m.username, m.organization, innerWidth) + "\n") b.WriteString("\n") // Menu items @@ -5087,38 +5091,27 @@ func (m mainMenuModel) View() string { cursor = "> " style = selectedStyle } - line := fmt.Sprintf("%s%-10s %s", cursor, choice.name, choice.description) + line := fmt.Sprintf("%s%s %s", cursor, choice.icon, choice.name) + if choice.description != "" { + line += " " + choice.description + } b.WriteString(style.Render(line) + "\n") + if i < len(m.choices)-1 { + b.WriteString("\n") + } } b.WriteString("\n") - // Status line - statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("7")) - b.WriteString(statusStyle.Render(fmt.Sprintf(" Status: %s", m.status)) + "\n") - - b.WriteString("\n") - // Help text - b.WriteString(dimStyle.Render(" Press Enter to select, q to quit") + "\n") - b.WriteString("\n") - - // Version - b.WriteString(dimStyle.Render(fmt.Sprintf(" %s (%s)", Version, BuildDate)) + "\n") - b.WriteString("\n") - - // Calculate box width - maxContentWidth := m.width - 4 - if maxContentWidth < 64 { - maxContentWidth = 64 - } + b.WriteString(dimStyle.Render("Press Enter to select, Ctrl+C to quit")) // Create border style borderStyle := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(borderColor). Padding(0, 1). - Width(maxContentWidth) + Width(boxContentWidth) return borderStyle.Render(b.String()) } @@ -5149,7 +5142,7 @@ func RunMainTUI(homeDir string) error { } if mm.runSetup { - if err := RunSetupMenu(homeDir); err != nil { + if err := RunSetupMenu(homeDir, mm.username, mm.organization); err != nil { // Log error but continue to menu slog.Error("Setup failed", "error", err) } @@ -5160,7 +5153,7 @@ func RunMainTUI(homeDir string) error { } if mm.runPull { - if err := runPullOperation(homeDir); err != nil { + if err := runPullOperation(homeDir, mm.username, mm.organization); err != nil { // Error already handled in runPullOperation slog.Error("Pull failed", "error", err) } @@ -5173,7 +5166,7 @@ func RunMainTUI(homeDir string) error { } // runPullOperation runs the pull operation from the TUI -func runPullOperation(homeDir string) error { +func runPullOperation(homeDir, username, org string) error { // Check for token token := os.Getenv("GITHUB_TOKEN") if token == "" { @@ -5186,14 +5179,15 @@ func runPullOperation(homeDir string) error { organization := os.Getenv("ORGANIZATION") if organization == "" { // Prompt for organization using a simple TUI - org, err := promptForOrganization(homeDir) + newOrg, err := promptForOrganization(homeDir) if err != nil { return err } - if org == "" { + if newOrg == "" { return fmt.Errorf("organization is required") } - organization = org + organization = newOrg + org = newOrg // Update the parameter too // Save organization to .env if err := saveOrganizationToEnv(homeDir, organization); err != nil { return fmt.Errorf("failed to save organization: %w", err) @@ -5216,7 +5210,7 @@ func runPullOperation(homeDir string) error { // Initialize progress display progress := NewUIProgress("Initializing GitHub offline MCP server...") - progress.InitItems(config) + progress.InitItems(config, username) // Set up slog to route to Bubble Tea UI slog.SetDefault(slog.New(NewBubbleTeaHandler(progress.program))) @@ -5411,12 +5405,8 @@ type orgPromptModel struct { cancelled bool width int height int - borderColors []lipgloss.AdaptiveColor - colorIndex int } -type orgPromptTickMsg time.Time - func newOrgPromptModel() orgPromptModel { ti := textinput.New() ti.Placeholder = "my-org" @@ -5427,21 +5417,14 @@ func newOrgPromptModel() orgPromptModel { ti.Focus() return orgPromptModel{ - textInput: ti, - width: 80, - height: 24, - borderColors: gradientColors, - colorIndex: 0, + textInput: ti, + width: 80, + height: 24, } } func (m orgPromptModel) Init() tea.Cmd { - return tea.Batch( - textinput.Blink, - tea.Tick(time.Second, func(t time.Time) tea.Msg { - return orgPromptTickMsg(t) - }), - ) + return textinput.Blink } func (m orgPromptModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -5463,13 +5446,7 @@ func (m orgPromptModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height - return m, nil - - case orgPromptTickMsg: - m.colorIndex = (m.colorIndex + 1) % len(m.borderColors) - return m, tea.Tick(time.Second, func(t time.Time) tea.Msg { - return orgPromptTickMsg(t) - }) + return m, tea.ClearScreen } m.textInput, cmd = m.textInput.Update(msg) @@ -5477,14 +5454,10 @@ func (m orgPromptModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m orgPromptModel) View() string { - borderColor := m.borderColors[m.colorIndex] var b strings.Builder - titleStyle := lipgloss.NewStyle().Bold(true) - dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) - - b.WriteString(titleStyle.Render(" GitHub 🧠 Pull") + "\n") + b.WriteString(titleStyle.Render(fmt.Sprintf("GitHub Brain %s / 📥 Pull", Version)) + "\n") b.WriteString("\n") b.WriteString(" Enter your GitHub organization:\n") b.WriteString(" " + m.textInput.View() + "\n") @@ -5610,14 +5583,11 @@ type loginModel struct { homeDir string width int height int - borderColors []lipgloss.AdaptiveColor - colorIndex int done bool } // Login message types type ( - loginTickMsg time.Time loginSuccessMsg struct{} loginErrorMsg struct{ err error } loginDeviceCodeMsg struct { @@ -5644,28 +5614,17 @@ func newLoginModel(homeDir string) loginModel { ti.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12")) return loginModel{ - spinner: s, - textInput: ti, - status: "waiting", - homeDir: homeDir, - width: 80, - height: 24, - borderColors: gradientColors, - colorIndex: 0, + spinner: s, + textInput: ti, + status: "waiting", + homeDir: homeDir, + width: 80, + height: 24, } } func (m loginModel) Init() tea.Cmd { - return tea.Batch( - m.spinner.Tick, - loginTickCmd(), - ) -} - -func loginTickCmd() tea.Cmd { - return tea.Tick(time.Second, func(t time.Time) tea.Msg { - return loginTickMsg(t) - }) + return m.spinner.Tick } func (m loginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -5692,11 +5651,7 @@ func (m loginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height - return m, nil - - case loginTickMsg: - m.colorIndex = (m.colorIndex + 1) % len(m.borderColors) - return m, loginTickCmd() + return m, tea.ClearScreen case loginDeviceCodeMsg: m.userCode = msg.userCode @@ -5745,8 +5700,6 @@ func (m loginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m loginModel) View() string { - borderColor := m.borderColors[m.colorIndex] - var content string switch m.status { @@ -5785,18 +5738,24 @@ func (m loginModel) View() string { func (m loginModel) renderWaitingView() string { var b strings.Builder - titleStyle := lipgloss.NewStyle().Bold(true) - b.WriteString(titleStyle.Render(" GitHub 🧠 Login") + "\n") + // Calculate spacing for title bar + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + b.WriteString(renderTitleBar("🔧 Setup", "", "", innerWidth) + "\n") b.WriteString("\n") - b.WriteString(" 🔐 GitHub Authentication (OAuth)\n") + b.WriteString("🔐 GitHub Authentication (OAuth)\n") b.WriteString("\n") if m.userCode == "" { - b.WriteString(" " + m.spinner.View() + " Requesting device code...\n") + b.WriteString(m.spinner.View() + " Requesting device code...\n") } else { - b.WriteString(" 1. Opening browser to: github.com/login/device\n") + b.WriteString("1. Opening browser to: github.com/login/device\n") b.WriteString("\n") - b.WriteString(" 2. Enter this code:\n") + b.WriteString("2. Enter this code:\n") b.WriteString("\n") // Code box with margin for alignment @@ -5805,15 +5764,15 @@ func (m loginModel) renderWaitingView() string { BorderForeground(lipgloss.Color("12")). Padding(0, 3). Bold(true). - MarginLeft(5) + MarginLeft(3) b.WriteString(codeStyle.Render(m.userCode) + "\n") b.WriteString("\n") - b.WriteString(" " + m.spinner.View() + " Waiting for authorization...\n") + b.WriteString(m.spinner.View() + " Waiting for authorization...\n") } b.WriteString("\n") - b.WriteString(" Press Ctrl+C to cancel\n") + b.WriteString("Press Ctrl+C to cancel\n") b.WriteString("\n") return b.String() @@ -5822,17 +5781,21 @@ func (m loginModel) renderWaitingView() string { func (m loginModel) renderOrgInputView() string { var b strings.Builder - titleStyle := lipgloss.NewStyle().Bold(true) - successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")) - - b.WriteString(titleStyle.Render(" GitHub 🧠 Login") + "\n") + // Calculate spacing for title bar + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + b.WriteString(renderTitleBar("🔧 Setup", m.username, "", innerWidth) + "\n") b.WriteString("\n") - b.WriteString(" " + successStyle.Render(fmt.Sprintf("✅ Successfully authenticated as @%s", m.username)) + "\n") + b.WriteString(successStyle.Render(fmt.Sprintf("✅ Successfully authenticated as @%s", m.username)) + "\n") b.WriteString("\n") - b.WriteString(" Enter your GitHub organization (optional):\n") - b.WriteString(" " + m.textInput.View() + "\n") + b.WriteString("Enter your GitHub organization (optional):\n") + b.WriteString(m.textInput.View() + "\n") b.WriteString("\n") - b.WriteString(" Press Enter to skip, or type organization name\n") + b.WriteString("Press Enter to skip, or type organization name\n") b.WriteString("\n") return b.String() @@ -5841,20 +5804,24 @@ func (m loginModel) renderOrgInputView() string { func (m loginModel) renderSuccessView() string { var b strings.Builder - titleStyle := lipgloss.NewStyle().Bold(true) - successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")) - - b.WriteString(titleStyle.Render(" GitHub 🧠 Login") + "\n") + // Calculate spacing for title bar + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + b.WriteString(renderTitleBar("🔧 Setup", m.username, m.organization, innerWidth) + "\n") b.WriteString("\n") - b.WriteString(" " + successStyle.Render("✅ Setup complete!") + "\n") + b.WriteString(successStyle.Render("✅ Setup complete!") + "\n") b.WriteString("\n") - b.WriteString(fmt.Sprintf(" Logged in as: @%s\n", m.username)) + b.WriteString(fmt.Sprintf("Logged in as: @%s\n", m.username)) if m.organization != "" { - b.WriteString(fmt.Sprintf(" Organization: %s\n", m.organization)) + b.WriteString(fmt.Sprintf("Organization: %s\n", m.organization)) } - b.WriteString(fmt.Sprintf(" Saved to: %s/.env\n", m.homeDir)) + b.WriteString(fmt.Sprintf("Saved to: %s/.env\n", m.homeDir)) b.WriteString("\n") - b.WriteString(" Press any key to continue...\n") + b.WriteString("Press any key to continue...\n") b.WriteString("\n") return b.String() @@ -5863,16 +5830,20 @@ func (m loginModel) renderSuccessView() string { func (m loginModel) renderErrorView() string { var b strings.Builder - titleStyle := lipgloss.NewStyle().Bold(true) - errorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("9")) - - b.WriteString(titleStyle.Render(" GitHub 🧠 Login") + "\n") + // Calculate spacing for title bar + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + b.WriteString(renderTitleBar("🔧 Setup", "", "", innerWidth) + "\n") b.WriteString("\n") - b.WriteString(" " + errorStyle.Render("❌ Authentication failed") + "\n") + b.WriteString(errorStyle.Render("❌ Authentication failed") + "\n") b.WriteString("\n") - b.WriteString(fmt.Sprintf(" Error: %s\n", m.errorMsg)) + b.WriteString(fmt.Sprintf("Error: %s\n", m.errorMsg)) b.WriteString("\n") - b.WriteString(" Please try again.\n") + b.WriteString("Please try again.\n") b.WriteString("\n") return b.String() @@ -5920,10 +5891,10 @@ type setupMenuModel struct { homeDir string choices []menuChoice cursor int + username string + organization string width int height int - borderColors []lipgloss.AdaptiveColor - colorIndex int quitting bool runOAuth bool runPAT bool @@ -5931,41 +5902,32 @@ type setupMenuModel struct { goBack bool } -// Message types for setup menu -type setupMenuTickMsg time.Time - -func newSetupMenuModel(homeDir string) setupMenuModel { +func newSetupMenuModel(homeDir, username, organization string) setupMenuModel { return setupMenuModel{ - homeDir: homeDir, + homeDir: homeDir, + username: username, + organization: organization, choices: []menuChoice{ - {name: "Login with GitHub (OAuth)", description: ""}, - {name: "Login with Personal Access Token", description: ""}, - {name: "Open configuration file", description: ""}, - {name: "← Back", description: ""}, + {icon: "🔗", name: "Login with GitHub (OAuth)", description: ""}, + {icon: "🔑", name: "Login with Personal Access Token", description: ""}, + {icon: "📄", name: "Open configuration file", description: ""}, + {icon: "←", name: "Back", description: ""}, }, - cursor: 0, - width: 80, - height: 24, - borderColors: gradientColors, - colorIndex: 0, + cursor: 0, + width: 80, + height: 24, } } func (m setupMenuModel) Init() tea.Cmd { - return setupMenuTickCmd() -} - -func setupMenuTickCmd() tea.Cmd { - return tea.Tick(time.Second, func(t time.Time) tea.Msg { - return setupMenuTickMsg(t) - }) + return nil } func (m setupMenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { - case "ctrl+c", "q": + case "ctrl+c": m.quitting = true return m, tea.Quit case "esc": @@ -5999,26 +5961,26 @@ func (m setupMenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height - return m, nil - - case setupMenuTickMsg: - m.colorIndex = (m.colorIndex + 1) % len(m.borderColors) - return m, setupMenuTickCmd() + return m, tea.ClearScreen } return m, nil } func (m setupMenuModel) View() string { - borderColor := m.borderColors[m.colorIndex] - var b strings.Builder - titleStyle := lipgloss.NewStyle().Bold(true) - dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")).Bold(true) - b.WriteString(titleStyle.Render(" GitHub 🧠 Setup") + "\n") + // Calculate box width for title bar + boxContentWidth := m.width - 2 + if boxContentWidth < 60 { + boxContentWidth = 60 + } + // Inner width is box content width minus padding (1 on each side) + innerWidth := boxContentWidth - 2 + + b.WriteString(renderTitleBar("🔧 Setup", m.username, m.organization, innerWidth) + "\n") b.WriteString("\n") // Menu items @@ -6029,35 +5991,32 @@ func (m setupMenuModel) View() string { cursor = "> " style = selectedStyle } - b.WriteString(style.Render(fmt.Sprintf("%s%s", cursor, choice.name)) + "\n") + line := fmt.Sprintf("%s%s %s", cursor, choice.icon, choice.name) + b.WriteString(style.Render(line) + "\n") + if i < len(m.choices)-1 { + b.WriteString("\n") + } } b.WriteString("\n") // Help text - b.WriteString(dimStyle.Render(" Press Enter to select, Esc to go back") + "\n") - b.WriteString("\n") - - // Calculate box width - maxContentWidth := m.width - 4 - if maxContentWidth < 64 { - maxContentWidth = 64 - } + b.WriteString(dimStyle.Render("Press Enter to select, Esc to go back") + "\n") // Create border style borderStyle := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(borderColor). Padding(0, 1). - Width(maxContentWidth) + Width(boxContentWidth) return borderStyle.Render(b.String()) } // RunSetupMenu runs the setup submenu -func RunSetupMenu(homeDir string) error { +func RunSetupMenu(homeDir, username, organization string) error { for { - m := newSetupMenuModel(homeDir) + m := newSetupMenuModel(homeDir, username, organization) p := tea.NewProgram(m, tea.WithAltScreen()) finalModel, err := p.Run() @@ -6135,14 +6094,11 @@ type patLoginModel struct { homeDir string width int height int - borderColors []lipgloss.AdaptiveColor - colorIndex int done bool } // PAT login message types type ( - patLoginTickMsg time.Time patTokenVerifiedMsg struct { username string token string @@ -6169,31 +6125,22 @@ func newPATLoginModel(homeDir string) patLoginModel { oi.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12")) return patLoginModel{ - textInput: ti, - orgInput: oi, - status: "token_input", - homeDir: homeDir, - width: 80, - height: 24, - borderColors: gradientColors, - colorIndex: 0, + textInput: ti, + orgInput: oi, + status: "token_input", + homeDir: homeDir, + width: 80, + height: 24, } } func (m patLoginModel) Init() tea.Cmd { return tea.Batch( textinput.Blink, - patLoginTickCmd(), openPATCreationPage(), ) } -func patLoginTickCmd() tea.Cmd { - return tea.Tick(time.Second, func(t time.Time) tea.Msg { - return patLoginTickMsg(t) - }) -} - func openPATCreationPage() tea.Cmd { return func() tea.Msg { // Open browser to pre-filled PAT creation page @@ -6243,11 +6190,7 @@ func (m patLoginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height - return m, nil - - case patLoginTickMsg: - m.colorIndex = (m.colorIndex + 1) % len(m.borderColors) - return m, patLoginTickCmd() + return m, tea.ClearScreen case patTokenVerifiedMsg: m.status = "org_input" @@ -6291,8 +6234,6 @@ func verifyPATToken(token string) tea.Cmd { } func (m patLoginModel) View() string { - borderColor := m.borderColors[m.colorIndex] - var content string switch m.status { @@ -6325,19 +6266,23 @@ func (m patLoginModel) View() string { func (m patLoginModel) renderTokenInputView() string { var b strings.Builder - titleStyle := lipgloss.NewStyle().Bold(true) - dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) - - b.WriteString(titleStyle.Render(" GitHub 🧠 Login") + "\n") + // Calculate spacing for title bar + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + b.WriteString(renderTitleBar("🔧 Setup", "", "", innerWidth) + "\n") b.WriteString("\n") - b.WriteString(" 🔑 Personal Access Token\n") + b.WriteString("🔑 Personal Access Token\n") b.WriteString("\n") - b.WriteString(" 1. Create a token at github.com (opened in browser)\n") + b.WriteString("1. Create a token at github.com (opened in browser)\n") b.WriteString("\n") - b.WriteString(" 2. Paste your token here:\n") - b.WriteString(" " + m.textInput.View() + "\n") + b.WriteString("2. Paste your token here:\n") + b.WriteString(m.textInput.View() + "\n") b.WriteString("\n") - b.WriteString(dimStyle.Render(" Press Enter to continue, Esc to cancel") + "\n") + b.WriteString(dimStyle.Render("Press Enter to continue, Esc to cancel") + "\n") b.WriteString("\n") return b.String() @@ -6346,17 +6291,21 @@ func (m patLoginModel) renderTokenInputView() string { func (m patLoginModel) renderOrgInputView() string { var b strings.Builder - titleStyle := lipgloss.NewStyle().Bold(true) - successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")) - - b.WriteString(titleStyle.Render(" GitHub 🧠 Login") + "\n") + // Calculate spacing for title bar + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + b.WriteString(renderTitleBar("🔧 Setup", m.username, "", innerWidth) + "\n") b.WriteString("\n") - b.WriteString(" " + successStyle.Render(fmt.Sprintf("✅ Successfully authenticated as @%s", m.username)) + "\n") + b.WriteString(successStyle.Render(fmt.Sprintf("✅ Successfully authenticated as @%s", m.username)) + "\n") b.WriteString("\n") - b.WriteString(" Enter your GitHub organization (optional):\n") - b.WriteString(" " + m.orgInput.View() + "\n") + b.WriteString("Enter your GitHub organization (optional):\n") + b.WriteString(m.orgInput.View() + "\n") b.WriteString("\n") - b.WriteString(" Press Enter to skip, or type organization name\n") + b.WriteString("Press Enter to skip, or type organization name\n") b.WriteString("\n") return b.String() @@ -6365,20 +6314,24 @@ func (m patLoginModel) renderOrgInputView() string { func (m patLoginModel) renderSuccessView() string { var b strings.Builder - titleStyle := lipgloss.NewStyle().Bold(true) - successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")) - - b.WriteString(titleStyle.Render(" GitHub 🧠 Login") + "\n") + // Calculate spacing for title bar + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + b.WriteString(renderTitleBar("🔧 Setup", m.username, m.organization, innerWidth) + "\n") b.WriteString("\n") - b.WriteString(" " + successStyle.Render("✅ Setup complete!") + "\n") + b.WriteString(successStyle.Render("✅ Setup complete!") + "\n") b.WriteString("\n") - b.WriteString(fmt.Sprintf(" Logged in as: @%s\n", m.username)) + b.WriteString(fmt.Sprintf("Logged in as: @%s\n", m.username)) if m.organization != "" { - b.WriteString(fmt.Sprintf(" Organization: %s\n", m.organization)) + b.WriteString(fmt.Sprintf("Organization: %s\n", m.organization)) } - b.WriteString(fmt.Sprintf(" Saved to: %s/.env\n", m.homeDir)) + b.WriteString(fmt.Sprintf("Saved to: %s/.env\n", m.homeDir)) b.WriteString("\n") - b.WriteString(" Press any key to continue...\n") + b.WriteString("Press any key to continue...\n") b.WriteString("\n") return b.String() @@ -6387,16 +6340,20 @@ func (m patLoginModel) renderSuccessView() string { func (m patLoginModel) renderErrorView() string { var b strings.Builder - titleStyle := lipgloss.NewStyle().Bold(true) - errorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("9")) - - b.WriteString(titleStyle.Render(" GitHub 🧠 Login") + "\n") + // Calculate spacing for title bar + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + b.WriteString(renderTitleBar("🔧 Setup", "", "", innerWidth) + "\n") b.WriteString("\n") - b.WriteString(" " + errorStyle.Render("❌ Authentication failed") + "\n") + b.WriteString(errorStyle.Render("❌ Authentication failed") + "\n") b.WriteString("\n") - b.WriteString(fmt.Sprintf(" Error: %s\n", m.errorMsg)) + b.WriteString(fmt.Sprintf("Error: %s\n", m.errorMsg)) b.WriteString("\n") - b.WriteString(" Please try again.\n") + b.WriteString("Please try again.\n") b.WriteString("\n") return b.String() diff --git a/main.md b/main.md index 28c5851..b753120 100644 --- a/main.md +++ b/main.md @@ -33,17 +33,28 @@ When `github-brain` is run without arguments, display an interactive menu: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub 🧠 │ +│ GitHub Brain 1.0.0 / 🏠 Home 👤 Not logged in │ │ │ -│ > Setup Configure authentication and settings │ -│ Pull Sync GitHub data to local database │ -│ Quit Exit │ +│ > 🔧 Setup Configure authentication and settings │ +│ 📥 Pull Sync GitHub data to local database │ +│ 🚪 Quit Exit │ │ │ -│ Status: Not logged in │ +│ Press Enter to select, Ctrl+C to quit │ │ │ -│ Press Enter to select, q to quit │ +╰────────────────────────────────────────────────────────────────╯ +``` + +After login but no organization configured: + +``` +╭────────────────────────────────────────────────────────────────╮ +│ GitHub Brain 1.0.0 / 🏠 Home 👤 @wham (no org) │ +│ │ +│ > 🔧 Setup Configure authentication and settings │ +│ 📥 Pull Sync GitHub data to local database │ +│ 🚪 Quit Exit │ │ │ -│ dev (unknown) │ +│ Press Enter to select, Ctrl+C to quit │ │ │ ╰────────────────────────────────────────────────────────────────╯ ``` @@ -52,54 +63,53 @@ After successful login with organization configured: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub 🧠 │ -│ │ -│ Setup Configure authentication and settings │ -│ > Pull Sync GitHub data to local database │ -│ Quit Exit │ -│ │ -│ Status: Logged in as @wham (my-org) │ +│ GitHub Brain 1.0.0 / 🏠 Home 👤 @wham (my-org) │ │ │ -│ Press Enter to select, q to quit │ +│ 🔧 Setup Configure authentication and settings │ +│ > 📥 Pull Sync GitHub data to local database │ +│ 🚪 Quit Exit │ │ │ -│ dev (unknown) │ +│ Press Enter to select, Ctrl+C to quit │ │ │ ╰────────────────────────────────────────────────────────────────╯ ``` +### Title Bar Format + +The title bar contains: + +- Left side: `GitHub Brain / ` +- Right side: `👤 ` (right-aligned) + +User status values: + +- `👤 Not logged in` - No GITHUB_TOKEN in .env or token invalid +- `👤 @username (no org)` - Token valid but no organization configured +- `👤 @username (org)` - Token and organization configured + ### Menu Navigation - Use arrow keys (↑/↓) or j/k to navigate - Press Enter to select - Press Esc to go back (in submenus) -- Press q or Ctrl+C to quit +- Press Ctrl+C to quit - Highlight current selection with `>` ### Menu Items -1. **Setup** - Opens the setup submenu (see [Setup Menu](#setup-menu) section) -2. **Pull** - Runs the pull operation (see [pull](#pull) section) -3. **Quit** - Exit the application +1. **🔧 Setup** - Opens the setup submenu (see [Setup Menu](#setup-menu) section) +2. **📥 Pull** - Runs the pull operation (see [pull](#pull) section) +3. **🚪 Quit** - Exit the application ### Default Selection - If user is logged in AND organization is configured → default to **Pull** - Otherwise → default to **Setup** -### Status Line - -Display current authentication status: - -- `Not logged in` - No GITHUB_TOKEN in .env -- `Logged in as @username` - Token exists and is valid, but no organization -- `Logged in as @username (org)` - Token and organization configured - -Check token validity on startup by making a GraphQL query for `viewer { login }`. - ### Flow 1. On startup, check if GITHUB_TOKEN exists and is valid -2. Show menu with appropriate status and default selection +2. Show menu with appropriate status in title bar and default selection 3. When user selects Setup, show the setup submenu 4. When user selects Pull, prompt for organization if not set, then run pull 5. After pull completes, return to menu @@ -111,23 +121,23 @@ The Setup submenu provides authentication and configuration options: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub 🧠 Setup │ +│ GitHub Brain 1.0.0 / 🔧 Setup 👤 Not logged in │ │ │ -│ > Login with GitHub (OAuth) │ -│ Login with Personal Access Token │ -│ Open configuration file │ -│ ← Back │ +│ > 🔗 Login with GitHub (OAuth) │ +│ 🔑 Login with Personal Access Token │ +│ 📄 Open configuration file │ +│ ← Back │ │ │ -│ Press Enter to select, Esc to go back │ +│ Press Enter to select, Esc to go back │ │ │ ╰────────────────────────────────────────────────────────────────╯ ``` ### Setup Menu Items -1. **Login with GitHub (OAuth)** - Runs the OAuth device flow (see [OAuth Login](#oauth-login) section) -2. **Login with Personal Access Token** - Manually enter a PAT (see [PAT Login](#pat-login) section) -3. **Open configuration file** - Opens `.env` file in default editor +1. **🔗 Login with GitHub (OAuth)** - Runs the OAuth device flow (see [OAuth Login](#oauth-login) section) +2. **🔑 Login with Personal Access Token** - Manually enter a PAT (see [PAT Login](#pat-login) section) +3. **📄 Open configuration file** - Opens `.env` file in default editor 4. **← Back** - Return to main menu ### Open Configuration File @@ -167,7 +177,6 @@ Use **Bubble Tea** framework (https://github.com/charmbracelet/bubbletea) for te - Animated spinner using `bubbles/spinner` with Dot style - Smooth color transitions for status changes (pending → active → complete) - Celebration emojis at milestones (✨ at 1000+ items, 🎉 at 5000+) - - Gradient animated borders (purple → blue → cyan) updated every second - Right-aligned comma-formatted counters ## OAuth Login @@ -379,19 +388,19 @@ Console at the beginning of pull: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub 🧠 Pull │ +│ GitHub Brain 1.0.0 / 📥 Pull 👤 @wham (my-org) │ │ │ -│ 📋 Repositories │ -│ 📋 Discussions │ -│ 📋 Issues │ -│ 📋 Pull-requests │ +│ 📋 Repositories │ +│ 📋 Discussions │ +│ 📋 Issues │ +│ 📋 Pull Requests │ │ │ -│ 📊 API Status ✅ 0 🟡 0 ❌ 0 │ -│ 🚀 Rate Limit ? / ? used, resets ? │ +│ 📊 API Status ✅ 0 🟡 0 ❌ 0 │ +│ 🚀 Rate Limit ? / ? used, resets ? │ │ │ -│ 💬 Activity │ -│ 21:37:12 ✨ Summoning data from the cloud... │ -│ 21:37:13 🔍 Fetching current user info │ +│ 💬 Activity │ +│ 21:37:12 ✨ Summoning data from the cloud... │ +│ 21:37:13 🔍 Fetching current user info │ │ │ │ │ │ │ @@ -403,22 +412,22 @@ Console during first item pull: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub 🧠 Pull │ +│ GitHub Brain 1.0.0 / 📥 Pull 👤 @wham (my-org) │ │ │ -│ ⠋ Repositories: 1,247 │ -│ 📋 Discussions │ -│ 📋 Issues │ -│ 📋 Pull-requests │ +│ ⠋ Repositories: 1,247 │ +│ 📋 Discussions │ +│ 📋 Issues │ +│ 📋 Pull Requests │ │ │ -│ 📊 API Status ✅ 120 🟡 1 ❌ 2 │ -│ 🚀 Rate Limit 1,000 / 5,000 used, resets in 2h 15m │ +│ 📊 API Status ✅ 120 🟡 1 ❌ 2 │ +│ 🚀 Rate Limit 1,000 / 5,000 used, resets in 2h 15m │ │ │ -│ 💬 Activity │ -│ 21:37:54 📦 Wrangling repositories... │ -│ 21:37:55 📄 Fetching page 12 │ -│ 21:37:56 💾 Processing batch 3 (repos 201-300) │ -│ 21:37:57 ⚡ Rate limit: 89% remaining │ -│ 21:37:58 ✨ Saved 47 repositories to database │ +│ 💬 Activity │ +│ 21:37:54 📦 Wrangling repositories... │ +│ 21:37:55 📄 Fetching page 12 │ +│ 21:37:56 💾 Processing batch 3 (repos 201-300) │ +│ 21:37:57 ⚡ Rate limit: 89% remaining │ +│ 21:37:58 ✨ Saved 47 repositories to database │ │ │ ╰────────────────────────────────────────────────────────────────╯ ``` @@ -427,22 +436,22 @@ Console when first item completes: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub 🧠 Pull │ +│ GitHub Brain 1.0.0 / 📥 Pull 👤 @wham (my-org) │ │ │ -│ ✅ Repositories: 2,847 │ -│ ⠙ Discussions: 156 │ -│ 📋 Issues │ -│ 📋 Pull-requests │ +│ ✅ Repositories: 2,847 │ +│ ⠙ Discussions: 156 │ +│ 📋 Issues │ +│ 📋 Pull Requests │ │ │ -│ 📊 API Status ✅ 160 🟡 1 ❌ 2 │ -│ 🚀 Rate Limit 1,500 / 5,000 used, resets in 1h 45m │ +│ 📊 API Status ✅ 160 🟡 1 ❌ 2 │ +│ 🚀 Rate Limit 1,500 / 5,000 used, resets in 1h 45m │ │ │ -│ 💬 Activity │ -│ 21:41:23 🎉 Repositories completed (2,847 synced) │ -│ 21:41:24 💬 Herding discussions... │ -│ 21:41:25 📄 Fetching from auth-service │ -│ 21:41:26 💾 Processing batch 1 │ -│ 21:41:27 ✨ Found 23 new discussions │ +│ 💬 Activity │ +│ 21:41:23 🎉 Repositories completed (2,847 synced) │ +│ 21:41:24 💬 Herding discussions... │ +│ 21:41:25 📄 Fetching from auth-service │ +│ 21:41:26 💾 Processing batch 1 │ +│ 21:41:27 ✨ Found 23 new discussions │ │ │ ╰────────────────────────────────────────────────────────────────╯ ``` @@ -451,22 +460,22 @@ Console when an error occurs: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub 🧠 Pull │ +│ GitHub Brain 1.0.0 / 📥 Pull 👤 @wham (my-org) │ │ │ -│ ✅ Repositories: 2,847 │ -│ ❌ Discussions: 156 (errors) │ -│ 📋 Issues │ -│ 📋 Pull-requests │ +│ ✅ Repositories: 2,847 │ +│ ❌ Discussions: 156 (errors) │ +│ 📋 Issues │ +│ 📋 Pull Requests │ │ │ -│ 📊 API Status ✅ 160 🟡 1 ❌ 5 │ -│ 🚀 Rate Limit 1,500 / 5,000 used, resets in 1h 45m │ +│ 📊 API Status ✅ 160 🟡 1 ❌ 5 │ +│ 🚀 Rate Limit 1,500 / 5,000 used, resets in 1h 45m │ │ │ -│ 💬 Activity │ -│ 21:42:15 ❌ API Error: Rate limit exceeded │ -│ 21:42:16 ⏳ Retrying in 30 seconds... │ -│ 21:42:47 ⚠️ Repository access denied: private-repo │ -│ 21:42:48 ➡️ Continuing with next repository... │ -│ 21:42:49 ❌ Failed to save discussion #4521 │ +│ 💬 Activity │ +│ 21:42:15 ❌ API Error: Rate limit exceeded │ +│ 21:42:16 ⏳ Retrying in 30 seconds... │ +│ 21:42:47 ⚠️ Repository access denied: private-repo │ +│ 21:42:48 ➡️ Continuing with next repository... │ +│ 21:42:49 ❌ Failed to save discussion #4521 │ │ │ ╰────────────────────────────────────────────────────────────────╯ ``` @@ -480,7 +489,7 @@ Console when an error occurs: ### Layout -- Gradient animated borders (purple → blue → cyan) updated every second +- Purple border color (#874BFD light / #7D56F4 dark) - Responsive width: `max(76, terminalWidth - 4)` - Box expands to full terminal width - Numbers formatted with commas: `1,247` @@ -503,7 +512,7 @@ Console when an error occurs: - Use standard lipgloss borders - no custom border painting or string manipulation - Rounded borders (╭╮╰╯) styled with `lipgloss.RoundedBorder()` - Title rendered as bold text inside the box, not embedded in border -- Border colors animated via `tickMsg` sent every second +- Static purple border color (#874BFD light / #7D56F4 dark) - Responsive width: `max(64, terminalWidth - 4)` **Spinners:** @@ -532,7 +541,7 @@ Console when an error occurs: **Color Scheme:** -- Purple/blue gradient for borders (via `borderColors` array) +- Purple border color (#874BFD light / #7D56F4 dark) - Bright blue (#12) for active items - Bright green (#10) for completed ✅ - Dim gray (#240) for skipped 🔕 diff --git a/scripts/run b/scripts/run index c18caef..2eb8312 100755 --- a/scripts/run +++ b/scripts/run @@ -7,7 +7,7 @@ echo "Running go vet..." go vet ./... || echo "Warning: go vet found issues (non-blocking in development)" # Build with FTS5 support enabled -CGO_ENABLED=1 CGO_CFLAGS="-DSQLITE_ENABLE_FTS5" CGO_LDFLAGS="-lm" go build -gcflags="all=-N -l" -o ./build/github-brain . +CGO_ENABLED=1 CGO_CFLAGS="-DSQLITE_ENABLE_FTS5" CGO_LDFLAGS="-Wl,-no_warn_duplicate_libraries" go build -gcflags="all=-N -l" -o ./build/github-brain . # Set home directory to checkout directory CHECKOUT_DIR="$(pwd)"