From 17314795fa7320ab8d71377b762cca3e6a68cf7f Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Fri, 26 Dec 2025 21:38:00 -0800 Subject: [PATCH 01/15] Update build instructions and remove VS Code tasks configuration --- .github/prompts/compile.prompt.md | 2 +- .vscode/tasks.json | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 .vscode/tasks.json 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/.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"] - } - ] -} From be472e54db2dbcacb467618f76929177c0b00b7e Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Fri, 26 Dec 2025 21:40:04 -0800 Subject: [PATCH 02/15] Fix linker flags in build script to suppress duplicate library warnings --- scripts/run | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)" From 53d645d5ed37cad7f8b6df971cff12167bf15b34 Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Fri, 26 Dec 2025 21:56:57 -0800 Subject: [PATCH 03/15] Refactor UI border color from gradient to static purple for consistency --- main.go | 219 ++++++++++++-------------------------------------------- main.md | 7 +- 2 files changed, 50 insertions(+), 176 deletions(-) diff --git a/main.go b/main.go index 79ece0a..9f995a2 100644 --- a/main.go +++ b/main.go @@ -62,15 +62,8 @@ 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"} // Removed ConsoleHandler - not needed with Bubble Tea @@ -4492,8 +4485,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 +4535,6 @@ type model struct { rateLimitReset time.Time width int height int - borderColors []lipgloss.AdaptiveColor - colorIndex int } // logEntry represents a timestamped log message (renamed from LogEntry to avoid conflict) @@ -4573,30 +4563,18 @@ func newModel(enabledItems map[string]bool) model { } return model{ - items: items, - itemOrder: itemOrder, - spinner: s, - logs: make([]logEntry, 0, 5), - width: 80, - height: 24, - borderColors: gradientColors, - colorIndex: 0, + items: items, + itemOrder: itemOrder, + spinner: s, + logs: make([]logEntry, 0, 5), + width: 80, + 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 @@ -4615,11 +4593,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.height = msg.Height return m, nil - case tickMsg: - // Rotate border color - m.colorIndex = (m.colorIndex + 1) % len(m.borderColors) - return m, tickCmd() - case itemUpdateMsg: if state, exists := m.items[msg.item]; exists { state.count = msg.count @@ -4706,7 +4679,6 @@ 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 @@ -4918,8 +4890,6 @@ type mainMenuModel struct { organization string width int height int - borderColors []lipgloss.AdaptiveColor - colorIndex int quitting bool runSetup bool runPull bool @@ -4932,14 +4902,11 @@ type menuChoice struct { } // 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{ @@ -4953,23 +4920,12 @@ func newMainMenuModel(homeDir string) mainMenuModel { 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 { @@ -5038,10 +4994,6 @@ func (m mainMenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.height = msg.Height return m, nil - case mainMenuTickMsg: - m.colorIndex = (m.colorIndex + 1) % len(m.borderColors) - return m, mainMenuTickCmd() - case authCheckResultMsg: m.checkingAuth = false if msg.loggedIn { @@ -5068,8 +5020,6 @@ 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) @@ -5411,12 +5361,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 +5373,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) { @@ -5464,12 +5403,6 @@ func (m orgPromptModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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) - }) } m.textInput, cmd = m.textInput.Update(msg) @@ -5477,7 +5410,6 @@ 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 @@ -5610,14 +5542,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 +5573,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) { @@ -5694,10 +5612,6 @@ func (m loginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.height = msg.Height return m, nil - case loginTickMsg: - m.colorIndex = (m.colorIndex + 1) % len(m.borderColors) - return m, loginTickCmd() - case loginDeviceCodeMsg: m.userCode = msg.userCode m.verificationURI = msg.verificationURI @@ -5745,8 +5659,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 { @@ -5917,23 +5829,18 @@ func RunLogin(homeDir string) error { // setupMenuModel is the Bubble Tea model for the setup submenu type setupMenuModel struct { - homeDir string - choices []menuChoice - cursor int - width int - height int - borderColors []lipgloss.AdaptiveColor - colorIndex int - quitting bool - runOAuth bool - runPAT bool - openConfig bool - goBack bool + homeDir string + choices []menuChoice + cursor int + width int + height int + quitting bool + runOAuth bool + runPAT bool + openConfig bool + goBack bool } -// Message types for setup menu -type setupMenuTickMsg time.Time - func newSetupMenuModel(homeDir string) setupMenuModel { return setupMenuModel{ homeDir: homeDir, @@ -5943,22 +5850,14 @@ func newSetupMenuModel(homeDir string) setupMenuModel { {name: "Open configuration file", description: ""}, {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) { @@ -6000,18 +5899,12 @@ func (m setupMenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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, nil } func (m setupMenuModel) View() string { - borderColor := m.borderColors[m.colorIndex] - var b strings.Builder titleStyle := lipgloss.NewStyle().Bold(true) @@ -6135,14 +6028,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 +6059,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 @@ -6245,10 +6126,6 @@ func (m patLoginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.height = msg.Height return m, nil - case patLoginTickMsg: - m.colorIndex = (m.colorIndex + 1) % len(m.borderColors) - return m, patLoginTickCmd() - case patTokenVerifiedMsg: m.status = "org_input" m.username = msg.username @@ -6291,8 +6168,6 @@ func verifyPATToken(token string) tea.Cmd { } func (m patLoginModel) View() string { - borderColor := m.borderColors[m.colorIndex] - var content string switch m.status { diff --git a/main.md b/main.md index 28c5851..61212c8 100644 --- a/main.md +++ b/main.md @@ -167,7 +167,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 @@ -480,7 +479,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 +502,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 +531,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 🔕 From b31843e2eb3ce8b1d522cfa4f1e43b30eb2f6bb8 Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Fri, 26 Dec 2025 22:01:18 -0800 Subject: [PATCH 04/15] Replace nil return with tea.ClearScreen on window size updates for better UI handling --- main.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 9f995a2..f32f166 100644 --- a/main.go +++ b/main.go @@ -4591,7 +4591,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height - return m, nil + return m, tea.ClearScreen case itemUpdateMsg: if state, exists := m.items[msg.item]; exists { @@ -4992,7 +4992,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 + return m, tea.ClearScreen case authCheckResultMsg: m.checkingAuth = false @@ -5402,7 +5402,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 + return m, tea.ClearScreen } m.textInput, cmd = m.textInput.Update(msg) @@ -5610,7 +5610,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 + return m, tea.ClearScreen case loginDeviceCodeMsg: m.userCode = msg.userCode @@ -5898,7 +5898,7 @@ 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 + return m, tea.ClearScreen } return m, nil @@ -6124,7 +6124,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 + return m, tea.ClearScreen case patTokenVerifiedMsg: m.status = "org_input" From 1b1cd5d789be2a3de129531eb46dccb63f29f6d9 Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Fri, 26 Dec 2025 22:04:10 -0800 Subject: [PATCH 05/15] Remove leading spaces in status and help text for cleaner UI presentation --- main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index f32f166..b5ca0aa 100644 --- a/main.go +++ b/main.go @@ -5026,7 +5026,7 @@ func (m mainMenuModel) View() string { dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")).Bold(true) - b.WriteString(titleStyle.Render(" GitHub 🧠") + "\n") + b.WriteString(titleStyle.Render("GitHub 🧠") + "\n") b.WriteString("\n") // Menu items @@ -5045,16 +5045,16 @@ func (m mainMenuModel) View() string { // Status line statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("7")) - b.WriteString(statusStyle.Render(fmt.Sprintf(" Status: %s", m.status)) + "\n") + 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(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(dimStyle.Render(fmt.Sprintf("%s (%s)", Version, BuildDate)) + "\n") b.WriteString("\n") // Calculate box width From be5e9876d82ba40fd7626cd615f20af4a8bfbf9c Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Fri, 26 Dec 2025 22:07:30 -0800 Subject: [PATCH 06/15] Refactor box width calculation and clean up UI text for improved presentation --- main.go | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/main.go b/main.go index b5ca0aa..ed7eee0 100644 --- a/main.go +++ b/main.go @@ -5055,12 +5055,12 @@ func (m mainMenuModel) View() string { // 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 + // Calculate box width: terminal width minus border (2 chars total) + // Width() in lipgloss sets width INCLUDING padding but EXCLUDING border + boxContentWidth := m.width - 2 + if boxContentWidth < 60 { + boxContentWidth = 60 } // Create border style @@ -5068,7 +5068,7 @@ func (m mainMenuModel) View() string { Border(lipgloss.RoundedBorder()). BorderForeground(borderColor). Padding(0, 1). - Width(maxContentWidth) + Width(boxContentWidth) return borderStyle.Render(b.String()) } @@ -5911,7 +5911,7 @@ func (m setupMenuModel) View() string { dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")).Bold(true) - b.WriteString(titleStyle.Render(" GitHub 🧠 Setup") + "\n") + b.WriteString(titleStyle.Render("GitHub 🧠 Setup") + "\n") b.WriteString("\n") // Menu items @@ -5928,13 +5928,12 @@ func (m setupMenuModel) View() string { b.WriteString("\n") // Help text - b.WriteString(dimStyle.Render(" Press Enter to select, Esc to go back") + "\n") - b.WriteString("\n") + b.WriteString(dimStyle.Render("Press Enter to select, Esc to go back") + "\n") - // Calculate box width - maxContentWidth := m.width - 4 - if maxContentWidth < 64 { - maxContentWidth = 64 + // Calculate box width: terminal width minus border (2 chars total) + boxContentWidth := m.width - 2 + if boxContentWidth < 60 { + boxContentWidth = 60 } // Create border style @@ -5942,7 +5941,7 @@ func (m setupMenuModel) View() string { Border(lipgloss.RoundedBorder()). BorderForeground(borderColor). Padding(0, 1). - Width(maxContentWidth) + Width(boxContentWidth) return borderStyle.Render(b.String()) } From ac39d282fa41278e452e5ad62d167d3f3543cbec Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Sat, 27 Dec 2025 00:00:51 -0800 Subject: [PATCH 07/15] Enhance UI by adding icons to menu choices and updating title styles for consistency --- main.go | 65 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/main.go b/main.go index ed7eee0..840fdf4 100644 --- a/main.go +++ b/main.go @@ -4782,7 +4782,7 @@ func (m model) View() string { // Add title as first line of content titleStyle := lipgloss.NewStyle().Bold(true) - titleLine := titleStyle.Render("GitHub 🧠 Pull") + titleLine := titleStyle.Render("GitHub Brain / 📥 Pull") // Pad title line to match content width titlePadding := maxContentWidth - visibleLength(titleLine) if titlePadding > 0 { @@ -4897,6 +4897,7 @@ type mainMenuModel struct { } type menuChoice struct { + icon string name string description string } @@ -4912,9 +4913,9 @@ 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...", @@ -5026,7 +5027,7 @@ func (m mainMenuModel) View() string { dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")).Bold(true) - b.WriteString(titleStyle.Render("GitHub 🧠") + "\n") + b.WriteString(titleStyle.Render("GitHub Brain / 🏠 Home") + "\n") b.WriteString("\n") // Menu items @@ -5037,8 +5038,14 @@ 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") @@ -5416,7 +5423,7 @@ func (m orgPromptModel) View() string { titleStyle := lipgloss.NewStyle().Bold(true) dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) - b.WriteString(titleStyle.Render(" GitHub 🧠 Pull") + "\n") + b.WriteString(titleStyle.Render("GitHub Brain / 📥 Pull") + "\n") b.WriteString("\n") b.WriteString(" Enter your GitHub organization:\n") b.WriteString(" " + m.textInput.View() + "\n") @@ -5737,9 +5744,9 @@ func (m loginModel) renderOrgInputView() string { titleStyle := lipgloss.NewStyle().Bold(true) successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")) - b.WriteString(titleStyle.Render(" GitHub 🧠 Login") + "\n") + b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ Setup") + "\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") @@ -5756,9 +5763,9 @@ func (m loginModel) renderSuccessView() string { titleStyle := lipgloss.NewStyle().Bold(true) successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")) - b.WriteString(titleStyle.Render(" GitHub 🧠 Login") + "\n") + b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ Setup") + "\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)) if m.organization != "" { @@ -5778,9 +5785,9 @@ func (m loginModel) renderErrorView() string { titleStyle := lipgloss.NewStyle().Bold(true) errorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("9")) - b.WriteString(titleStyle.Render(" GitHub 🧠 Login") + "\n") + b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ Setup") + "\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("\n") @@ -5845,10 +5852,10 @@ func newSetupMenuModel(homeDir string) setupMenuModel { return setupMenuModel{ homeDir: homeDir, 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, @@ -5911,7 +5918,7 @@ func (m setupMenuModel) View() string { dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")).Bold(true) - b.WriteString(titleStyle.Render("GitHub 🧠 Setup") + "\n") + b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ Setup") + "\n") b.WriteString("\n") // Menu items @@ -5922,7 +5929,11 @@ 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") @@ -6202,9 +6213,9 @@ func (m patLoginModel) renderTokenInputView() string { titleStyle := lipgloss.NewStyle().Bold(true) dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) - b.WriteString(titleStyle.Render(" GitHub 🧠 Login") + "\n") + b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ Setup") + "\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("\n") @@ -6223,9 +6234,9 @@ func (m patLoginModel) renderOrgInputView() string { titleStyle := lipgloss.NewStyle().Bold(true) successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")) - b.WriteString(titleStyle.Render(" GitHub 🧠 Login") + "\n") + b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ Setup") + "\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") @@ -6242,9 +6253,9 @@ func (m patLoginModel) renderSuccessView() string { titleStyle := lipgloss.NewStyle().Bold(true) successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")) - b.WriteString(titleStyle.Render(" GitHub 🧠 Login") + "\n") + b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ Setup") + "\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)) if m.organization != "" { @@ -6264,9 +6275,9 @@ func (m patLoginModel) renderErrorView() string { titleStyle := lipgloss.NewStyle().Bold(true) errorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("9")) - b.WriteString(titleStyle.Render(" GitHub 🧠 Login") + "\n") + b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ Setup") + "\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("\n") From a32f60c227cf24c8e0df0730caa45cd71dbeaf2b Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Sat, 27 Dec 2025 18:50:32 -0800 Subject: [PATCH 08/15] Fix spacing in menu choice display for improved readability --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 840fdf4..c48261b 100644 --- a/main.go +++ b/main.go @@ -5038,7 +5038,7 @@ func (m mainMenuModel) View() string { cursor = "> " style = selectedStyle } - line := fmt.Sprintf("%s%s %s", cursor, choice.icon, choice.name) + line := fmt.Sprintf("%s%s %s", cursor, choice.icon, choice.name) if choice.description != "" { line += " " + choice.description } @@ -5929,7 +5929,7 @@ func (m setupMenuModel) View() string { cursor = "> " style = selectedStyle } - line := fmt.Sprintf("%s%s %s", cursor, choice.icon, choice.name) + 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") From 7461e6dce409d10e6784566d56b24e07658b0c11 Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Sat, 27 Dec 2025 18:51:51 -0800 Subject: [PATCH 09/15] Update testing guide for clarity and detail on verifying code changes --- .github/skills/testing/SKILL.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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). From c53b31eff0807642fabd68c4f421ea69fb5c78ef Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Sat, 27 Dec 2025 18:57:27 -0800 Subject: [PATCH 10/15] Enhance menu display by adding icons for improved visual clarity and consistency --- main.md | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/main.md b/main.md index 61212c8..679a119 100644 --- a/main.md +++ b/main.md @@ -33,17 +33,17 @@ When `github-brain` is run without arguments, display an interactive menu: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub 🧠 │ +│ GitHub Brain / 🏠 Home │ │ │ -│ > 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 │ +│ Status: Not logged in │ │ │ -│ Press Enter to select, q to quit │ +│ Press Enter to select, q to quit │ │ │ -│ dev (unknown) │ +│ dev (unknown) │ │ │ ╰────────────────────────────────────────────────────────────────╯ ``` @@ -52,17 +52,17 @@ After successful login with organization configured: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub 🧠 │ +│ GitHub Brain / 🏠 Home │ │ │ -│ 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: Logged in as @wham (my-org) │ +│ Status: Logged in as @wham (my-org) │ │ │ -│ Press Enter to select, q to quit │ +│ Press Enter to select, q to quit │ │ │ -│ dev (unknown) │ +│ dev (unknown) │ │ │ ╰────────────────────────────────────────────────────────────────╯ ``` @@ -77,9 +77,9 @@ After successful login with organization configured: ### 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 @@ -111,23 +111,23 @@ The Setup submenu provides authentication and configuration options: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub 🧠 Setup │ +│ GitHub Brain / 🔧 Setup │ │ │ -│ > 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 From 3b99944a3abb182b20c0dbf4eed948a5acba8c7f Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Sat, 27 Dec 2025 19:52:05 -0800 Subject: [PATCH 11/15] Enhance UI title bar to display user status and organization information for improved user experience --- main.go | 394 ++++++++++++++++++++++++++++++++++++++++++-------------- main.md | 148 +++++++++++---------- 2 files changed, 381 insertions(+), 161 deletions(-) diff --git a/main.go b/main.go index c48261b..27f1d23 100644 --- a/main.go +++ b/main.go @@ -4348,7 +4348,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) @@ -4381,13 +4381,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()) @@ -4535,6 +4535,8 @@ type model struct { rateLimitReset time.Time width int height int + username string + organization string } // logEntry represents a timestamped log message (renamed from LogEntry to avoid conflict) @@ -4544,7 +4546,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 @@ -4563,11 +4565,13 @@ func newModel(enabledItems map[string]bool) model { } return model{ - items: items, - itemOrder: itemOrder, - spinner: s, - logs: make([]logEntry, 0, 5), - width: 80, + items: items, + itemOrder: itemOrder, + spinner: s, + logs: make([]logEntry, 0, 5), + width: 80, + username: username, + organization: organization, height: 24, } } @@ -4782,12 +4786,27 @@ func (m model) View() string { // Add title as first line of content titleStyle := lipgloss.NewStyle().Bold(true) - titleLine := titleStyle.Render("GitHub Brain / 📥 Pull") - // Pad title line to match content width - titlePadding := maxContentWidth - visibleLength(titleLine) - if titlePadding > 0 { - titleLine = titleLine + strings.Repeat(" ", titlePadding) + leftTitle := "GitHub Brain / 📥 Pull" + 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") @@ -5027,7 +5046,37 @@ func (m mainMenuModel) View() string { dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")).Bold(true) - b.WriteString(titleStyle.Render("GitHub Brain / 🏠 Home") + "\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 + + // Build title bar with left and right parts + leftTitle := "GitHub Brain / 🏠 Home" + 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: innerWidth - leftTitle visual width - rightStatus visual width + leftWidth := lipgloss.Width(leftTitle) + rightWidth := lipgloss.Width(rightStatus) + spacing := innerWidth - leftWidth - rightWidth + if spacing < 1 { + spacing = 1 + } + + titleLine := titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + b.WriteString(titleLine + "\n") b.WriteString("\n") // Menu items @@ -5050,12 +5099,6 @@ func (m mainMenuModel) View() string { 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") @@ -5063,13 +5106,6 @@ func (m mainMenuModel) View() string { // Version b.WriteString(dimStyle.Render(fmt.Sprintf("%s (%s)", Version, BuildDate)) + "\n") - // Calculate box width: terminal width minus border (2 chars total) - // Width() in lipgloss sets width INCLUDING padding but EXCLUDING border - boxContentWidth := m.width - 2 - if boxContentWidth < 60 { - boxContentWidth = 60 - } - // Create border style borderStyle := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). @@ -5106,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) } @@ -5117,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) } @@ -5130,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 == "" { @@ -5143,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) @@ -5173,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))) @@ -5705,17 +5742,34 @@ 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 + + leftTitle := "GitHub Brain / 🔧 Setup" + rightStatus := "👤 Not logged in" + leftWidth := lipgloss.Width(leftTitle) + rightWidth := lipgloss.Width(rightStatus) + spacing := innerWidth - leftWidth - rightWidth + if spacing < 1 { + spacing = 1 + } + + b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\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 @@ -5724,15 +5778,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() @@ -5744,14 +5798,30 @@ func (m loginModel) renderOrgInputView() string { titleStyle := lipgloss.NewStyle().Bold(true) successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")) - b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ Setup") + "\n") + // Calculate spacing for title bar + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + leftTitle := "GitHub Brain / 🔧 Setup" + rightStatus := fmt.Sprintf("👤 @%s (no org)", m.username) + leftWidth := lipgloss.Width(leftTitle) + rightWidth := lipgloss.Width(rightStatus) + spacing := innerWidth - leftWidth - rightWidth + if spacing < 1 { + spacing = 1 + } + + b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\n") b.WriteString("\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() @@ -5763,17 +5833,38 @@ func (m loginModel) renderSuccessView() string { titleStyle := lipgloss.NewStyle().Bold(true) successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")) - b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ Setup") + "\n") + // Calculate spacing for title bar + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + leftTitle := "GitHub Brain / 🔧 Setup" + var rightStatus string + if m.organization != "" { + rightStatus = fmt.Sprintf("👤 @%s (%s)", m.username, m.organization) + } else { + rightStatus = fmt.Sprintf("👤 @%s (no org)", m.username) + } + leftWidth := lipgloss.Width(leftTitle) + rightWidth := lipgloss.Width(rightStatus) + spacing := innerWidth - leftWidth - rightWidth + if spacing < 1 { + spacing = 1 + } + + b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\n") b.WriteString("\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() @@ -5785,13 +5876,29 @@ func (m loginModel) renderErrorView() string { titleStyle := lipgloss.NewStyle().Bold(true) errorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("9")) - b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ Setup") + "\n") + // Calculate spacing for title bar + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + leftTitle := "GitHub Brain / 🔧 Setup" + rightStatus := "👤 Not logged in" + leftWidth := lipgloss.Width(leftTitle) + rightWidth := lipgloss.Width(rightStatus) + spacing := innerWidth - leftWidth - rightWidth + if spacing < 1 { + spacing = 1 + } + + b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\n") b.WriteString("\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() @@ -5836,21 +5943,25 @@ func RunLogin(homeDir string) error { // setupMenuModel is the Bubble Tea model for the setup submenu type setupMenuModel struct { - homeDir string - choices []menuChoice - cursor int - width int - height int - quitting bool - runOAuth bool - runPAT bool - openConfig bool - goBack bool -} - -func newSetupMenuModel(homeDir string) setupMenuModel { + homeDir string + choices []menuChoice + cursor int + username string + organization string + width int + height int + quitting bool + runOAuth bool + runPAT bool + openConfig bool + goBack bool +} + +func newSetupMenuModel(homeDir, username, organization string) setupMenuModel { return setupMenuModel{ - homeDir: homeDir, + homeDir: homeDir, + username: username, + organization: organization, choices: []menuChoice{ {icon: "🔗", name: "Login with GitHub (OAuth)", description: ""}, {icon: "🔑", name: "Login with Personal Access Token", description: ""}, @@ -5918,7 +6029,37 @@ func (m setupMenuModel) View() string { dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")).Bold(true) - b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ 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 + + // Build title bar with left and right parts + leftTitle := "GitHub Brain / 🔧 Setup" + 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 + leftWidth := lipgloss.Width(leftTitle) + rightWidth := lipgloss.Width(rightStatus) + spacing := innerWidth - leftWidth - rightWidth + if spacing < 1 { + spacing = 1 + } + + titleLine := titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + b.WriteString(titleLine + "\n") b.WriteString("\n") // Menu items @@ -5941,12 +6082,6 @@ func (m setupMenuModel) View() string { // Help text b.WriteString(dimStyle.Render("Press Enter to select, Esc to go back") + "\n") - // Calculate box width: terminal width minus border (2 chars total) - boxContentWidth := m.width - 2 - if boxContentWidth < 60 { - boxContentWidth = 60 - } - // Create border style borderStyle := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). @@ -5958,9 +6093,9 @@ func (m setupMenuModel) View() 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() @@ -6213,16 +6348,32 @@ func (m patLoginModel) renderTokenInputView() string { titleStyle := lipgloss.NewStyle().Bold(true) dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) - b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ Setup") + "\n") + // Calculate spacing for title bar + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + leftTitle := "GitHub Brain / 🔧 Setup" + rightStatus := "👤 Not logged in" + leftWidth := lipgloss.Width(leftTitle) + rightWidth := lipgloss.Width(rightStatus) + spacing := innerWidth - leftWidth - rightWidth + if spacing < 1 { + spacing = 1 + } + + b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\n") b.WriteString("\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() @@ -6234,14 +6385,30 @@ func (m patLoginModel) renderOrgInputView() string { titleStyle := lipgloss.NewStyle().Bold(true) successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")) - b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ Setup") + "\n") + // Calculate spacing for title bar + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + leftTitle := "GitHub Brain / 🔧 Setup" + rightStatus := fmt.Sprintf("👤 @%s (no org)", m.username) + leftWidth := lipgloss.Width(leftTitle) + rightWidth := lipgloss.Width(rightStatus) + spacing := innerWidth - leftWidth - rightWidth + if spacing < 1 { + spacing = 1 + } + + b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\n") b.WriteString("\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() @@ -6253,17 +6420,38 @@ func (m patLoginModel) renderSuccessView() string { titleStyle := lipgloss.NewStyle().Bold(true) successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")) - b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ Setup") + "\n") + // Calculate spacing for title bar + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + leftTitle := "GitHub Brain / 🔧 Setup" + var rightStatus string + if m.organization != "" { + rightStatus = fmt.Sprintf("👤 @%s (%s)", m.username, m.organization) + } else { + rightStatus = fmt.Sprintf("👤 @%s (no org)", m.username) + } + leftWidth := lipgloss.Width(leftTitle) + rightWidth := lipgloss.Width(rightStatus) + spacing := innerWidth - leftWidth - rightWidth + if spacing < 1 { + spacing = 1 + } + + b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\n") b.WriteString("\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() @@ -6275,13 +6463,29 @@ func (m patLoginModel) renderErrorView() string { titleStyle := lipgloss.NewStyle().Bold(true) errorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("9")) - b.WriteString(titleStyle.Render("GitHub Brain / ⚙️ Setup") + "\n") + // Calculate spacing for title bar + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + leftTitle := "GitHub Brain / 🔧 Setup" + rightStatus := "👤 Not logged in" + leftWidth := lipgloss.Width(leftTitle) + rightWidth := lipgloss.Width(rightStatus) + spacing := innerWidth - leftWidth - rightWidth + if spacing < 1 { + spacing = 1 + } + + b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\n") b.WriteString("\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 679a119..78be0b5 100644 --- a/main.md +++ b/main.md @@ -33,13 +33,28 @@ When `github-brain` is run without arguments, display an interactive menu: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub Brain / 🏠 Home │ +│ GitHub Brain / 🏠 Home 👤 Not logged in │ │ │ │ > 🔧 Setup Configure authentication and settings │ │ 📥 Pull Sync GitHub data to local database │ │ 🚪 Quit Exit │ │ │ -│ Status: Not logged in │ +│ Press Enter to select, q to quit │ +│ │ +│ dev (unknown) │ +│ │ +╰────────────────────────────────────────────────────────────────╯ +``` + +After login but no organization configured: + +``` +╭────────────────────────────────────────────────────────────────╮ +│ GitHub Brain / 🏠 Home 👤 @wham (no org) │ +│ │ +│ > 🔧 Setup Configure authentication and settings │ +│ 📥 Pull Sync GitHub data to local database │ +│ 🚪 Quit Exit │ │ │ │ Press Enter to select, q to quit │ │ │ @@ -52,14 +67,12 @@ After successful login with organization configured: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub Brain / 🏠 Home │ +│ GitHub Brain / 🏠 Home 👤 @wham (my-org) │ │ │ │ 🔧 Setup Configure authentication and settings │ │ > 📥 Pull Sync GitHub data to local database │ │ 🚪 Quit Exit │ │ │ -│ Status: Logged in as @wham (my-org) │ -│ │ │ Press Enter to select, q to quit │ │ │ │ dev (unknown) │ @@ -67,6 +80,19 @@ After successful login with organization configured: ╰────────────────────────────────────────────────────────────────╯ ``` +### 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 @@ -86,20 +112,10 @@ After successful login with organization configured: - 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,7 +127,7 @@ The Setup submenu provides authentication and configuration options: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub Brain / 🔧 Setup │ +│ GitHub Brain / 🔧 Setup 👤 Not logged in │ │ │ │ > 🔗 Login with GitHub (OAuth) │ │ 🔑 Login with Personal Access Token │ @@ -378,19 +394,19 @@ Console at the beginning of pull: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub 🧠 Pull │ +│ GitHub Brain / 📥 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 │ │ │ │ │ │ │ @@ -402,22 +418,22 @@ Console during first item pull: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub 🧠 Pull │ +│ GitHub Brain / 📥 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 │ │ │ ╰────────────────────────────────────────────────────────────────╯ ``` @@ -426,22 +442,22 @@ Console when first item completes: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub 🧠 Pull │ +│ GitHub Brain / 📥 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 │ │ │ ╰────────────────────────────────────────────────────────────────╯ ``` @@ -450,22 +466,22 @@ Console when an error occurs: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub 🧠 Pull │ +│ GitHub Brain / 📥 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 │ │ │ ╰────────────────────────────────────────────────────────────────╯ ``` From e266c4391b3f537dfae3edbb69563bcff5b79168 Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:07:48 -0800 Subject: [PATCH 12/15] Update quit instructions to use Ctrl+C instead of 'q' for consistency across the application --- main.go | 8 ++++---- main.md | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index 27f1d23..23ad8dd 100644 --- a/main.go +++ b/main.go @@ -4588,7 +4588,7 @@ 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 } @@ -4984,7 +4984,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": @@ -5100,7 +5100,7 @@ func (m mainMenuModel) View() string { b.WriteString("\n") // Help text - b.WriteString(dimStyle.Render("Press Enter to select, q to quit") + "\n") + b.WriteString(dimStyle.Render("Press Enter to select, Ctrl+C to quit") + "\n") b.WriteString("\n") // Version @@ -5982,7 +5982,7 @@ 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": diff --git a/main.md b/main.md index 78be0b5..14c66b2 100644 --- a/main.md +++ b/main.md @@ -39,7 +39,7 @@ When `github-brain` is run without arguments, display an interactive menu: │ 📥 Pull Sync GitHub data to local database │ │ 🚪 Quit Exit │ │ │ -│ Press Enter to select, q to quit │ +│ Press Enter to select, Ctrl+C to quit │ │ │ │ dev (unknown) │ │ │ @@ -56,7 +56,7 @@ After login but no organization configured: │ 📥 Pull Sync GitHub data to local database │ │ 🚪 Quit Exit │ │ │ -│ Press Enter to select, q to quit │ +│ Press Enter to select, Ctrl+C to quit │ │ │ │ dev (unknown) │ │ │ @@ -73,7 +73,7 @@ After successful login with organization configured: │ > 📥 Pull Sync GitHub data to local database │ │ 🚪 Quit Exit │ │ │ -│ Press Enter to select, q to quit │ +│ Press Enter to select, Ctrl+C to quit │ │ │ │ dev (unknown) │ │ │ @@ -98,7 +98,7 @@ User status values: - 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 From d547086ea65c97aaa1f3074b4b2c1a858a1203f6 Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:18:32 -0800 Subject: [PATCH 13/15] Update title bar to include version number for improved clarity --- main.go | 28 ++++++++++++---------------- main.md | 24 +++++++++--------------- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/main.go b/main.go index 23ad8dd..d467bf7 100644 --- a/main.go +++ b/main.go @@ -4786,7 +4786,7 @@ func (m model) View() string { // Add title as first line of content titleStyle := lipgloss.NewStyle().Bold(true) - leftTitle := "GitHub Brain / 📥 Pull" + leftTitle := fmt.Sprintf("GitHub Brain %s / 📥 Pull", Version) var rightStatus string if m.username != "" { if m.organization != "" { @@ -5055,7 +5055,7 @@ func (m mainMenuModel) View() string { innerWidth := boxContentWidth - 2 // Build title bar with left and right parts - leftTitle := "GitHub Brain / 🏠 Home" + leftTitle := fmt.Sprintf("GitHub Brain %s / 🏠 Home", Version) var rightStatus string if m.username != "" { if m.organization != "" { @@ -5101,10 +5101,6 @@ func (m mainMenuModel) View() string { // Help text b.WriteString(dimStyle.Render("Press Enter to select, Ctrl+C to quit") + "\n") - b.WriteString("\n") - - // Version - b.WriteString(dimStyle.Render(fmt.Sprintf("%s (%s)", Version, BuildDate)) + "\n") // Create border style borderStyle := lipgloss.NewStyle(). @@ -5460,7 +5456,7 @@ func (m orgPromptModel) View() string { titleStyle := lipgloss.NewStyle().Bold(true) dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) - b.WriteString(titleStyle.Render("GitHub Brain / 📥 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") @@ -5750,7 +5746,7 @@ func (m loginModel) renderWaitingView() string { } innerWidth := maxContentWidth - 2 - leftTitle := "GitHub Brain / 🔧 Setup" + leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) rightStatus := "👤 Not logged in" leftWidth := lipgloss.Width(leftTitle) rightWidth := lipgloss.Width(rightStatus) @@ -5805,7 +5801,7 @@ func (m loginModel) renderOrgInputView() string { } innerWidth := maxContentWidth - 2 - leftTitle := "GitHub Brain / 🔧 Setup" + leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) rightStatus := fmt.Sprintf("👤 @%s (no org)", m.username) leftWidth := lipgloss.Width(leftTitle) rightWidth := lipgloss.Width(rightStatus) @@ -5840,7 +5836,7 @@ func (m loginModel) renderSuccessView() string { } innerWidth := maxContentWidth - 2 - leftTitle := "GitHub Brain / 🔧 Setup" + leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) var rightStatus string if m.organization != "" { rightStatus = fmt.Sprintf("👤 @%s (%s)", m.username, m.organization) @@ -5883,7 +5879,7 @@ func (m loginModel) renderErrorView() string { } innerWidth := maxContentWidth - 2 - leftTitle := "GitHub Brain / 🔧 Setup" + leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) rightStatus := "👤 Not logged in" leftWidth := lipgloss.Width(leftTitle) rightWidth := lipgloss.Width(rightStatus) @@ -6038,7 +6034,7 @@ func (m setupMenuModel) View() string { innerWidth := boxContentWidth - 2 // Build title bar with left and right parts - leftTitle := "GitHub Brain / 🔧 Setup" + leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) var rightStatus string if m.username != "" { if m.organization != "" { @@ -6355,7 +6351,7 @@ func (m patLoginModel) renderTokenInputView() string { } innerWidth := maxContentWidth - 2 - leftTitle := "GitHub Brain / 🔧 Setup" + leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) rightStatus := "👤 Not logged in" leftWidth := lipgloss.Width(leftTitle) rightWidth := lipgloss.Width(rightStatus) @@ -6392,7 +6388,7 @@ func (m patLoginModel) renderOrgInputView() string { } innerWidth := maxContentWidth - 2 - leftTitle := "GitHub Brain / 🔧 Setup" + leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) rightStatus := fmt.Sprintf("👤 @%s (no org)", m.username) leftWidth := lipgloss.Width(leftTitle) rightWidth := lipgloss.Width(rightStatus) @@ -6427,7 +6423,7 @@ func (m patLoginModel) renderSuccessView() string { } innerWidth := maxContentWidth - 2 - leftTitle := "GitHub Brain / 🔧 Setup" + leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) var rightStatus string if m.organization != "" { rightStatus = fmt.Sprintf("👤 @%s (%s)", m.username, m.organization) @@ -6470,7 +6466,7 @@ func (m patLoginModel) renderErrorView() string { } innerWidth := maxContentWidth - 2 - leftTitle := "GitHub Brain / 🔧 Setup" + leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) rightStatus := "👤 Not logged in" leftWidth := lipgloss.Width(leftTitle) rightWidth := lipgloss.Width(rightStatus) diff --git a/main.md b/main.md index 14c66b2..b753120 100644 --- a/main.md +++ b/main.md @@ -33,7 +33,7 @@ When `github-brain` is run without arguments, display an interactive menu: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub Brain / 🏠 Home 👤 Not logged in │ +│ GitHub Brain 1.0.0 / 🏠 Home 👤 Not logged in │ │ │ │ > 🔧 Setup Configure authentication and settings │ │ 📥 Pull Sync GitHub data to local database │ @@ -41,8 +41,6 @@ When `github-brain` is run without arguments, display an interactive menu: │ │ │ Press Enter to select, Ctrl+C to quit │ │ │ -│ dev (unknown) │ -│ │ ╰────────────────────────────────────────────────────────────────╯ ``` @@ -50,7 +48,7 @@ After login but no organization configured: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub Brain / 🏠 Home 👤 @wham (no org) │ +│ GitHub Brain 1.0.0 / 🏠 Home 👤 @wham (no org) │ │ │ │ > 🔧 Setup Configure authentication and settings │ │ 📥 Pull Sync GitHub data to local database │ @@ -58,8 +56,6 @@ After login but no organization configured: │ │ │ Press Enter to select, Ctrl+C to quit │ │ │ -│ dev (unknown) │ -│ │ ╰────────────────────────────────────────────────────────────────╯ ``` @@ -67,7 +63,7 @@ After successful login with organization configured: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub Brain / 🏠 Home 👤 @wham (my-org) │ +│ GitHub Brain 1.0.0 / 🏠 Home 👤 @wham (my-org) │ │ │ │ 🔧 Setup Configure authentication and settings │ │ > 📥 Pull Sync GitHub data to local database │ @@ -75,8 +71,6 @@ After successful login with organization configured: │ │ │ Press Enter to select, Ctrl+C to quit │ │ │ -│ dev (unknown) │ -│ │ ╰────────────────────────────────────────────────────────────────╯ ``` @@ -84,7 +78,7 @@ After successful login with organization configured: The title bar contains: -- Left side: `GitHub Brain / ` +- Left side: `GitHub Brain / ` - Right side: `👤 ` (right-aligned) User status values: @@ -127,7 +121,7 @@ The Setup submenu provides authentication and configuration options: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub Brain / 🔧 Setup 👤 Not logged in │ +│ GitHub Brain 1.0.0 / 🔧 Setup 👤 Not logged in │ │ │ │ > 🔗 Login with GitHub (OAuth) │ │ 🔑 Login with Personal Access Token │ @@ -394,7 +388,7 @@ Console at the beginning of pull: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub Brain / 📥 Pull 👤 @wham (my-org) │ +│ GitHub Brain 1.0.0 / 📥 Pull 👤 @wham (my-org) │ │ │ │ 📋 Repositories │ │ 📋 Discussions │ @@ -418,7 +412,7 @@ Console during first item pull: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub Brain / 📥 Pull 👤 @wham (my-org) │ +│ GitHub Brain 1.0.0 / 📥 Pull 👤 @wham (my-org) │ │ │ │ ⠋ Repositories: 1,247 │ │ 📋 Discussions │ @@ -442,7 +436,7 @@ Console when first item completes: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub Brain / 📥 Pull 👤 @wham (my-org) │ +│ GitHub Brain 1.0.0 / 📥 Pull 👤 @wham (my-org) │ │ │ │ ✅ Repositories: 2,847 │ │ ⠙ Discussions: 156 │ @@ -466,7 +460,7 @@ Console when an error occurs: ``` ╭────────────────────────────────────────────────────────────────╮ -│ GitHub Brain / 📥 Pull 👤 @wham (my-org) │ +│ GitHub Brain 1.0.0 / 📥 Pull 👤 @wham (my-org) │ │ │ │ ✅ Repositories: 2,847 │ │ ❌ Discussions: 156 (errors) │ From f4b022a524a71fea444f036de3a89dd0cbf1f56c Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:21:33 -0800 Subject: [PATCH 14/15] Update help text to remove extra newline for improved formatting --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index d467bf7..7ebb79e 100644 --- a/main.go +++ b/main.go @@ -5100,7 +5100,7 @@ func (m mainMenuModel) View() string { b.WriteString("\n") // Help text - b.WriteString(dimStyle.Render("Press Enter to select, Ctrl+C to quit") + "\n") + b.WriteString(dimStyle.Render("Press Enter to select, Ctrl+C to quit")) // Create border style borderStyle := lipgloss.NewStyle(). From 35f98daf19a11d75691a2ddd8b4fe42dac987126 Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Sat, 27 Dec 2025 21:16:16 -0800 Subject: [PATCH 15/15] Refactor title bar rendering to improve user status display and streamline UI styles --- main.go | 226 ++++++++++++-------------------------------------------- 1 file changed, 49 insertions(+), 177 deletions(-) diff --git a/main.go b/main.go index 7ebb79e..17b7fe5 100644 --- a/main.go +++ b/main.go @@ -65,6 +65,39 @@ var ( // 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 // BubbleTeaHandler is a custom slog handler that routes logs to Bubble Tea UI @@ -4682,11 +4715,7 @@ func (m *model) addLog(message string) { // View renders the UI func (m model) View() string { - // Define colors and styles - 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 @@ -4698,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)) @@ -4785,7 +4814,6 @@ func (m model) View() string { } // Add title as first line of content - titleStyle := lipgloss.NewStyle().Bold(true) leftTitle := fmt.Sprintf("GitHub Brain %s / 📥 Pull", Version) var rightStatus string if m.username != "" { @@ -4824,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 @@ -4841,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 @@ -4864,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 ", @@ -5042,8 +5070,6 @@ func (m mainMenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m mainMenuModel) View() string { 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) // Calculate box width for title bar @@ -5054,29 +5080,7 @@ func (m mainMenuModel) View() string { // Inner width is box content width minus padding (1 on each side) innerWidth := boxContentWidth - 2 - // Build title bar with left and right parts - leftTitle := fmt.Sprintf("GitHub Brain %s / 🏠 Home", 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: innerWidth - leftTitle visual width - rightStatus visual width - leftWidth := lipgloss.Width(leftTitle) - rightWidth := lipgloss.Width(rightStatus) - spacing := innerWidth - leftWidth - rightWidth - if spacing < 1 { - spacing = 1 - } - - titleLine := titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) - b.WriteString(titleLine + "\n") + b.WriteString(renderTitleBar("🏠 Home", m.username, m.organization, innerWidth) + "\n") b.WriteString("\n") // Menu items @@ -5453,9 +5457,6 @@ func (m orgPromptModel) View() string { var b strings.Builder - titleStyle := lipgloss.NewStyle().Bold(true) - dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) - b.WriteString(titleStyle.Render(fmt.Sprintf("GitHub Brain %s / 📥 Pull", Version)) + "\n") b.WriteString("\n") b.WriteString(" Enter your GitHub organization:\n") @@ -5737,8 +5738,6 @@ func (m loginModel) View() string { func (m loginModel) renderWaitingView() string { var b strings.Builder - titleStyle := lipgloss.NewStyle().Bold(true) - // Calculate spacing for title bar maxContentWidth := m.width - 4 if maxContentWidth < 64 { @@ -5746,16 +5745,7 @@ func (m loginModel) renderWaitingView() string { } innerWidth := maxContentWidth - 2 - leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) - rightStatus := "👤 Not logged in" - leftWidth := lipgloss.Width(leftTitle) - rightWidth := lipgloss.Width(rightStatus) - spacing := innerWidth - leftWidth - rightWidth - if spacing < 1 { - spacing = 1 - } - - b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\n") + b.WriteString(renderTitleBar("🔧 Setup", "", "", innerWidth) + "\n") b.WriteString("\n") b.WriteString("🔐 GitHub Authentication (OAuth)\n") b.WriteString("\n") @@ -5791,9 +5781,6 @@ 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")) - // Calculate spacing for title bar maxContentWidth := m.width - 4 if maxContentWidth < 64 { @@ -5801,16 +5788,7 @@ func (m loginModel) renderOrgInputView() string { } innerWidth := maxContentWidth - 2 - leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) - rightStatus := fmt.Sprintf("👤 @%s (no org)", m.username) - leftWidth := lipgloss.Width(leftTitle) - rightWidth := lipgloss.Width(rightStatus) - spacing := innerWidth - leftWidth - rightWidth - if spacing < 1 { - spacing = 1 - } - - b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\n") + 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("\n") @@ -5826,9 +5804,6 @@ 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")) - // Calculate spacing for title bar maxContentWidth := m.width - 4 if maxContentWidth < 64 { @@ -5836,21 +5811,7 @@ func (m loginModel) renderSuccessView() string { } innerWidth := maxContentWidth - 2 - leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) - var rightStatus string - if m.organization != "" { - rightStatus = fmt.Sprintf("👤 @%s (%s)", m.username, m.organization) - } else { - rightStatus = fmt.Sprintf("👤 @%s (no org)", m.username) - } - leftWidth := lipgloss.Width(leftTitle) - rightWidth := lipgloss.Width(rightStatus) - spacing := innerWidth - leftWidth - rightWidth - if spacing < 1 { - spacing = 1 - } - - b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\n") + b.WriteString(renderTitleBar("🔧 Setup", m.username, m.organization, innerWidth) + "\n") b.WriteString("\n") b.WriteString(successStyle.Render("✅ Setup complete!") + "\n") b.WriteString("\n") @@ -5869,9 +5830,6 @@ 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")) - // Calculate spacing for title bar maxContentWidth := m.width - 4 if maxContentWidth < 64 { @@ -5879,16 +5837,7 @@ func (m loginModel) renderErrorView() string { } innerWidth := maxContentWidth - 2 - leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) - rightStatus := "👤 Not logged in" - leftWidth := lipgloss.Width(leftTitle) - rightWidth := lipgloss.Width(rightStatus) - spacing := innerWidth - leftWidth - rightWidth - if spacing < 1 { - spacing = 1 - } - - b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\n") + b.WriteString(renderTitleBar("🔧 Setup", "", "", innerWidth) + "\n") b.WriteString("\n") b.WriteString(errorStyle.Render("❌ Authentication failed") + "\n") b.WriteString("\n") @@ -6021,8 +5970,6 @@ func (m setupMenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m setupMenuModel) View() string { 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) // Calculate box width for title bar @@ -6033,29 +5980,7 @@ func (m setupMenuModel) View() string { // Inner width is box content width minus padding (1 on each side) innerWidth := boxContentWidth - 2 - // Build title bar with left and right parts - leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", 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 - leftWidth := lipgloss.Width(leftTitle) - rightWidth := lipgloss.Width(rightStatus) - spacing := innerWidth - leftWidth - rightWidth - if spacing < 1 { - spacing = 1 - } - - titleLine := titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) - b.WriteString(titleLine + "\n") + b.WriteString(renderTitleBar("🔧 Setup", m.username, m.organization, innerWidth) + "\n") b.WriteString("\n") // Menu items @@ -6341,9 +6266,6 @@ 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")) - // Calculate spacing for title bar maxContentWidth := m.width - 4 if maxContentWidth < 64 { @@ -6351,16 +6273,7 @@ func (m patLoginModel) renderTokenInputView() string { } innerWidth := maxContentWidth - 2 - leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) - rightStatus := "👤 Not logged in" - leftWidth := lipgloss.Width(leftTitle) - rightWidth := lipgloss.Width(rightStatus) - spacing := innerWidth - leftWidth - rightWidth - if spacing < 1 { - spacing = 1 - } - - b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\n") + b.WriteString(renderTitleBar("🔧 Setup", "", "", innerWidth) + "\n") b.WriteString("\n") b.WriteString("🔑 Personal Access Token\n") b.WriteString("\n") @@ -6378,9 +6291,6 @@ 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")) - // Calculate spacing for title bar maxContentWidth := m.width - 4 if maxContentWidth < 64 { @@ -6388,16 +6298,7 @@ func (m patLoginModel) renderOrgInputView() string { } innerWidth := maxContentWidth - 2 - leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) - rightStatus := fmt.Sprintf("👤 @%s (no org)", m.username) - leftWidth := lipgloss.Width(leftTitle) - rightWidth := lipgloss.Width(rightStatus) - spacing := innerWidth - leftWidth - rightWidth - if spacing < 1 { - spacing = 1 - } - - b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\n") + 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("\n") @@ -6413,9 +6314,6 @@ 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")) - // Calculate spacing for title bar maxContentWidth := m.width - 4 if maxContentWidth < 64 { @@ -6423,21 +6321,7 @@ func (m patLoginModel) renderSuccessView() string { } innerWidth := maxContentWidth - 2 - leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) - var rightStatus string - if m.organization != "" { - rightStatus = fmt.Sprintf("👤 @%s (%s)", m.username, m.organization) - } else { - rightStatus = fmt.Sprintf("👤 @%s (no org)", m.username) - } - leftWidth := lipgloss.Width(leftTitle) - rightWidth := lipgloss.Width(rightStatus) - spacing := innerWidth - leftWidth - rightWidth - if spacing < 1 { - spacing = 1 - } - - b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\n") + b.WriteString(renderTitleBar("🔧 Setup", m.username, m.organization, innerWidth) + "\n") b.WriteString("\n") b.WriteString(successStyle.Render("✅ Setup complete!") + "\n") b.WriteString("\n") @@ -6456,9 +6340,6 @@ 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")) - // Calculate spacing for title bar maxContentWidth := m.width - 4 if maxContentWidth < 64 { @@ -6466,16 +6347,7 @@ func (m patLoginModel) renderErrorView() string { } innerWidth := maxContentWidth - 2 - leftTitle := fmt.Sprintf("GitHub Brain %s / 🔧 Setup", Version) - rightStatus := "👤 Not logged in" - leftWidth := lipgloss.Width(leftTitle) - rightWidth := lipgloss.Width(rightStatus) - spacing := innerWidth - leftWidth - rightWidth - if spacing < 1 { - spacing = 1 - } - - b.WriteString(titleStyle.Render(leftTitle) + strings.Repeat(" ", spacing) + titleStyle.Render(rightStatus) + "\n") + b.WriteString(renderTitleBar("🔧 Setup", "", "", innerWidth) + "\n") b.WriteString("\n") b.WriteString(errorStyle.Render("❌ Authentication failed") + "\n") b.WriteString("\n")