From 8713f5b3945f002cdb03dcd8a3daa0ac2cb64932 Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:15:14 -0800 Subject: [PATCH 01/11] Refactor login flow to navigate to organization selection after token save --- main.go | 660 +++++++++++++++++++++++++++++++++++++++++++------------- main.md | 139 ++++++++++-- 2 files changed, 623 insertions(+), 176 deletions(-) diff --git a/main.go b/main.go index d6461dd..bf3e36b 100644 --- a/main.go +++ b/main.go @@ -5561,14 +5561,12 @@ type AccessTokenResponse struct { // loginModel is the Bubble Tea model for the login UI type loginModel struct { spinner spinner.Model - textInput textinput.Model userCode string verificationURI string - status string // "waiting", "org_input", "success", "error" + status string // "waiting", "select_org", "success", "error" errorMsg string username string token string - organization string homeDir string width int height int @@ -5589,7 +5587,6 @@ type ( username string token string } - loginOrgSubmittedMsg struct{} ) func newLoginModel(homeDir, currentUsername, currentOrg string) loginModel { @@ -5597,16 +5594,8 @@ func newLoginModel(homeDir, currentUsername, currentOrg string) loginModel { s.Spinner = spinner.Dot s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("12")) - ti := textinput.New() - ti.Placeholder = "my-org" - ti.CharLimit = 100 - ti.Width = 30 - ti.Prompt = "> " - ti.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12")) - return loginModel{ spinner: s, - textInput: ti, status: "waiting", homeDir: homeDir, width: 80, @@ -5640,15 +5629,6 @@ func (m loginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.done = true return m, tea.Quit } - if m.status == "org_input" { - m.organization = strings.TrimSpace(m.textInput.Value()) - return m, func() tea.Msg { return loginOrgSubmittedMsg{} } - } - } - // Pass key messages to textinput when in org_input mode - if m.status == "org_input" { - m.textInput, cmd = m.textInput.Update(msg) - return m, cmd } case tea.WindowSizeMsg: @@ -5662,26 +5642,19 @@ func (m loginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case loginAuthenticatedMsg: - // User has authenticated, now prompt for organization - m.status = "org_input" + // User has authenticated, save token and go to org selection m.username = msg.username m.token = msg.token - m.textInput.Focus() - return m, textinput.Blink - - case loginOrgSubmittedMsg: - // Save token and organization to .env - if err := saveTokenToEnv(m.homeDir, m.token, m.organization); err != nil { + // Save token to .env (organization will be set by Select Organization screen) + if err := saveTokenToEnv(m.homeDir, m.token, ""); err != nil { m.status = "error" m.errorMsg = fmt.Sprintf("failed to save token: %v", err) m.done = true return m, nil } - m.status = "success" + m.status = "select_org" m.done = true - return m, tea.Tick(2*time.Second, func(t time.Time) tea.Msg { - return tea.Quit() - }) + return m, tea.Quit case loginSuccessMsg: m.status = "success" @@ -5708,8 +5681,6 @@ func (m loginModel) View() string { switch m.status { case "waiting": content = m.renderWaitingView() - case "org_input": - content = m.renderOrgInputView() case "success": content = m.renderSuccessView() case "error": @@ -5786,29 +5757,6 @@ func (m loginModel) renderWaitingView() string { return b.String() } -func (m loginModel) renderOrgInputView() string { - var b strings.Builder - - // Calculate spacing for title bar - maxContentWidth := m.width - 4 - if maxContentWidth < 64 { - maxContentWidth = 64 - } - innerWidth := maxContentWidth - 2 - - b.WriteString(renderTitleBar("๐Ÿ”ง Setup", m.username, "", innerWidth) + "\n") - b.WriteString("\n") - b.WriteString(successStyle.Render(fmt.Sprintf("โœ… Successfully authenticated as @%s", m.username)) + "\n") - b.WriteString("\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("\n") - - return b.String() -} - func (m loginModel) renderSuccessView() string { var b strings.Builder @@ -5819,18 +5767,13 @@ func (m loginModel) renderSuccessView() string { } innerWidth := maxContentWidth - 2 - b.WriteString(renderTitleBar("๐Ÿ”ง Setup", m.username, m.organization, innerWidth) + "\n") + b.WriteString(renderTitleBar("๐Ÿ”ง Setup", m.username, "", innerWidth) + "\n") b.WriteString("\n") - b.WriteString(successStyle.Render("โœ… Setup complete!") + "\n") + b.WriteString(successStyle.Render("โœ… Token saved!") + "\n") b.WriteString("\n") 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("Saved to: %s/.env\n", m.homeDir)) b.WriteString("\n") - b.WriteString("Press any key to continue...\n") - b.WriteString("\n") return b.String() } @@ -5888,6 +5831,13 @@ func RunLogin(homeDir, currentUsername, currentOrg string) error { if lm.status == "cancelled" { return nil // Go back without error } + if lm.status == "select_org" { + // Reload .env to pick up the saved token + envPath := homeDir + "/.env" + _ = godotenv.Load(envPath) + // Navigate to Select Organization screen + return runSelectOrgWithFlag(homeDir, lm.username, true) + } if lm.status != "success" { return fmt.Errorf("login cancelled") } @@ -5902,34 +5852,45 @@ func RunLogin(homeDir, currentUsername, currentOrg string) error { // setupMenuModel is the Bubble Tea model for the setup submenu type setupMenuModel struct { - homeDir string - choices []menuChoice - cursor int - username string - organization string - width int - height int - quitting bool - runOAuth bool - runPAT bool - openConfig bool - goBack bool + homeDir string + choices []menuChoice + cursor int + username string + organization string + width int + height int + quitting bool + runOAuth bool + runPAT bool + runSelectOrg bool + openConfig bool + goBack bool } func newSetupMenuModel(homeDir, username, organization string, cursor int) setupMenuModel { + choices := []menuChoice{ + {icon: "โœจ", name: "Login with device", description: "Recommended for organization owners"}, + {icon: "๐Ÿ”‘", name: "Login with PAT", description: "Works without organization ownership"}, + } + + // Only show "Select organization" when logged in + if username != "" { + choices = append(choices, menuChoice{icon: "๐Ÿข", name: "Select organization", description: "Choose organization to sync"}) + } + + choices = append(choices, + menuChoice{icon: "๐Ÿ“", name: "Advanced", description: "Edit configuration file"}, + menuChoice{icon: "โ†", name: "Back", description: "Esc"}, + ) + return setupMenuModel{ homeDir: homeDir, username: username, organization: organization, - choices: []menuChoice{ - {icon: "โœจ", name: "Login with device", description: "Recommended for organization owners"}, - {icon: "๐Ÿ”‘", name: "Login with PAT", description: "Works without organization ownership"}, - {icon: "๐Ÿ“", name: "Advanced", description: "Edit configuration file"}, - {icon: "โ†", name: "Back", description: "Esc"}, - }, - cursor: cursor, - width: 80, - height: 24, + choices: choices, + cursor: cursor, + width: 80, + height: 24, } } @@ -5956,17 +5917,22 @@ func (m setupMenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.cursor++ } case "enter": - switch m.cursor { - case 0: // OAuth Login + // Handle selection based on choice name (since order varies) + selectedChoice := m.choices[m.cursor].name + switch selectedChoice { + case "Login with device": m.runOAuth = true return m, tea.Quit - case 1: // PAT Login + case "Login with PAT": m.runPAT = true return m, tea.Quit - case 2: // Open config + case "Select organization": + m.runSelectOrg = true + return m, tea.Quit + case "Advanced": m.openConfig = true return m, tea.Quit - case 3: // Back + case "Back": m.goBack = true return m, tea.Quit } @@ -6089,6 +6055,19 @@ func RunSetupMenu(homeDir, username, organization string) error { return nil // Return to main menu after login } + if sm.runSelectOrg { + if err := RunSelectOrg(homeDir, username); err != nil { + if err.Error() == "quit" { + return err // Propagate quit to exit app + } + slog.Error("Select organization failed", "error", err) + } + // Reload .env after selection + envPath := homeDir + "/.env" + _ = godotenv.Load(envPath) + return nil // Return to main menu after selection + } + if sm.openConfig { if err := openConfigFile(homeDir); err != nil { slog.Error("Failed to open config file", "error", err) @@ -6114,6 +6093,427 @@ func openConfigFile(homeDir string) error { return browser.OpenFile(envPath) } +// ============================================================================ +// Select Organization Implementation +// ============================================================================ + +// selectOrgModel is the Bubble Tea model for the organization selection UI +type selectOrgModel struct { + spinner spinner.Model + textInput textinput.Model + organizations []string // all organizations from API + filtered []string // filtered organizations based on text input + cursor int + status string // "loading", "list", "error", "success" + errorMsg string + username string + homeDir string + width int + height int + done bool + selectedOrg string + fromLogin bool // whether this was invoked after login flow +} + +// Select organization message types +type ( + orgsLoadedMsg struct { + organizations []string + } + orgsLoadErrorMsg struct { + err error + } + orgSelectedMsg struct { + organization string + } +) + +func newSelectOrgModel(homeDir, username string, fromLogin bool) selectOrgModel { + s := spinner.New() + s.Spinner = spinner.Dot + s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("12")) + + ti := textinput.New() + ti.Placeholder = "" + ti.CharLimit = 100 + ti.Width = 30 + ti.Prompt = "" + ti.Focus() + + return selectOrgModel{ + spinner: s, + textInput: ti, + status: "loading", + username: username, + homeDir: homeDir, + fromLogin: fromLogin, + width: 80, + height: 24, + } +} + +func (m selectOrgModel) Init() tea.Cmd { + return tea.Batch( + m.spinner.Tick, + textinput.Blink, + fetchOrganizations(), + ) +} + +func fetchOrganizations() tea.Cmd { + return func() tea.Msg { + token := os.Getenv("GITHUB_TOKEN") + if token == "" { + return orgsLoadErrorMsg{err: fmt.Errorf("no GitHub token found")} + } + + src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + httpClient := oauth2.NewClient(context.Background(), src) + client := githubv4.NewClient(httpClient) + + var query struct { + Viewer struct { + Organizations struct { + Nodes []struct { + Login string + } + } `graphql:"organizations(first: 10)"` + } + } + + if err := client.Query(context.Background(), &query, nil); err != nil { + return orgsLoadErrorMsg{err: err} + } + + var orgs []string + for _, org := range query.Viewer.Organizations.Nodes { + orgs = append(orgs, org.Login) + } + + return orgsLoadedMsg{organizations: orgs} + } +} + +func (m selectOrgModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c": + m.status = "quit" + m.done = true + return m, tea.Quit + case "esc": + m.status = "cancelled" + m.done = true + return m, tea.Quit + case "up", "ctrl+p": + if m.status == "list" && m.cursor > 0 { + m.cursor-- + } + case "down", "ctrl+n": + if m.status == "list" && len(m.filtered) > 0 && m.cursor < len(m.filtered)-1 { + m.cursor++ + } + case "enter": + if m.status == "list" { + var org string + inputValue := strings.TrimSpace(m.textInput.Value()) + + if len(m.filtered) > 0 && m.cursor < len(m.filtered) { + // Select from filtered list + org = m.filtered[m.cursor] + } else if inputValue != "" { + // Use the typed value + org = inputValue + } + + if org != "" { + m.selectedOrg = org + return m, func() tea.Msg { return orgSelectedMsg{organization: org} } + } + } + } + + // Pass other key messages to textinput when in list mode + if m.status == "list" { + prevValue := m.textInput.Value() + m.textInput, cmd = m.textInput.Update(msg) + + // If text changed, update filtered list and reset cursor + if m.textInput.Value() != prevValue { + m.filtered = m.filterOrganizations(m.textInput.Value()) + m.cursor = 0 + } + return m, cmd + } + + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + return m, tea.ClearScreen + + case orgsLoadedMsg: + m.organizations = msg.organizations + m.filtered = msg.organizations + m.status = "list" + m.cursor = 0 + return m, textinput.Blink + + case orgsLoadErrorMsg: + // On error, show empty list with text input + m.organizations = nil + m.filtered = nil + m.status = "list" + m.cursor = 0 + return m, textinput.Blink + + case orgSelectedMsg: + // Save organization to .env + if err := saveOrgToEnv(m.homeDir, msg.organization); err != nil { + m.status = "error" + m.errorMsg = fmt.Sprintf("failed to save organization: %v", err) + return m, nil + } + m.status = "success" + m.done = true + return m, tea.Tick(500*time.Millisecond, func(t time.Time) tea.Msg { + return tea.Quit() + }) + + case spinner.TickMsg: + if m.status == "loading" { + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd + } + } + + return m, nil +} + +// filterOrganizations filters the organization list based on input +func (m selectOrgModel) filterOrganizations(input string) []string { + if input == "" { + return m.organizations + } + input = strings.ToLower(input) + var filtered []string + for _, org := range m.organizations { + if strings.Contains(strings.ToLower(org), input) { + filtered = append(filtered, org) + } + } + return filtered +} + +func (m selectOrgModel) View() string { + var content string + + switch m.status { + case "loading": + content = m.renderLoadingView() + case "list": + content = m.renderListView() + case "error": + content = m.renderErrorView() + case "success": + content = m.renderSuccessView() + } + + // Calculate box width + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + + // Create border style + borderStyle := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(borderColor). + Padding(0, 1). + Width(maxContentWidth) + + return borderStyle.Render(content) +} + +func (m selectOrgModel) renderLoadingView() string { + var b strings.Builder + + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + b.WriteString(renderTitleBar("๐Ÿข Select organization", m.username, "", innerWidth) + "\n") + b.WriteString("\n") + b.WriteString(m.spinner.View() + " Loading organizations...\n") + b.WriteString("\n") + b.WriteString(dimStyle.Render("Press Esc to cancel") + "\n") + + return b.String() +} + +func (m selectOrgModel) renderListView() string { + var b strings.Builder + + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + b.WriteString(renderTitleBar("๐Ÿข Select organization", m.username, "", innerWidth) + "\n") + b.WriteString("\n") + + selectorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")) + selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")).Bold(true) + + // Show filtered organizations + if len(m.filtered) == 0 && len(m.organizations) == 0 { + b.WriteString(dimStyle.Render(" No organizations found") + "\n") + } else if len(m.filtered) == 0 { + b.WriteString(dimStyle.Render(" No matches") + "\n") + } else { + for i, org := range m.filtered { + cursor := " " + style := dimStyle + if m.cursor == i { + cursor = selectorStyle.Render("โ–ถ") + " " + style = selectedStyle + } + b.WriteString(cursor + style.Render(org) + "\n") + } + } + + b.WriteString("\n") + + // Text input for manual entry + b.WriteString(dimStyle.Render(" Or enter manually: ") + m.textInput.View() + "\n") + b.WriteString("\n") + + // Help text + if len(m.filtered) > 0 { + b.WriteString(dimStyle.Render("โ†‘โ†“ navigate ยท Enter select ยท type to filter ยท Esc back") + "\n") + } else { + b.WriteString(dimStyle.Render("Enter organization name ยท Esc back") + "\n") + } + + return b.String() +} + +func (m selectOrgModel) renderErrorView() string { + var b strings.Builder + + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + b.WriteString(renderTitleBar("๐Ÿข Select organization", m.username, "", innerWidth) + "\n") + b.WriteString("\n") + b.WriteString(errorStyle.Render("โŒ Error") + "\n") + b.WriteString("\n") + b.WriteString(fmt.Sprintf("Error: %s\n", m.errorMsg)) + b.WriteString("\n") + + return b.String() +} + +func (m selectOrgModel) renderSuccessView() string { + var b strings.Builder + + maxContentWidth := m.width - 4 + if maxContentWidth < 64 { + maxContentWidth = 64 + } + innerWidth := maxContentWidth - 2 + + b.WriteString(renderTitleBar("๐Ÿข Select organization", m.username, m.selectedOrg, innerWidth) + "\n") + b.WriteString("\n") + b.WriteString(successStyle.Render("โœ… Organization saved!") + "\n") + b.WriteString("\n") + b.WriteString(fmt.Sprintf("Organization: %s\n", m.selectedOrg)) + b.WriteString("\n") + + return b.String() +} + +// RunSelectOrg runs the organization selection flow +func RunSelectOrg(homeDir, username string) error { + return runSelectOrgWithFlag(homeDir, username, false) +} + +// runSelectOrgWithFlag runs the organization selection flow with fromLogin flag +func runSelectOrgWithFlag(homeDir, username string, fromLogin bool) error { + m := newSelectOrgModel(homeDir, username, fromLogin) + p := tea.NewProgram(m, tea.WithAltScreen()) + + finalModel, err := p.Run() + if err != nil { + return fmt.Errorf("UI error: %w", err) + } + + if sm, ok := finalModel.(selectOrgModel); ok { + if sm.status == "quit" { + return fmt.Errorf("quit") + } + if sm.status == "error" { + return fmt.Errorf("%s", sm.errorMsg) + } + if sm.status == "cancelled" { + return nil // Go back without error + } + } + + return nil +} + +// saveOrgToEnv saves the organization to the .env file +func saveOrgToEnv(homeDir string, organization string) error { + envPath := homeDir + "/.env" + + // Read existing .env content + existingContent, err := os.ReadFile(envPath) + if err != nil && !os.IsNotExist(err) { + return err + } + + orgLine := fmt.Sprintf("ORGANIZATION=%s", organization) + + if len(existingContent) == 0 { + // File doesn't exist or is empty + return os.WriteFile(envPath, []byte(orgLine+"\n"), 0600) + } + + // Process existing content + lines := strings.Split(string(existingContent), "\n") + var newLines []string + orgFound := false + + for _, line := range lines { + if strings.HasPrefix(line, "ORGANIZATION=") { + newLines = append(newLines, orgLine) + orgFound = true + } else if line != "" { + newLines = append(newLines, line) + } + } + + if !orgFound { + newLines = append(newLines, orgLine) + } + + newContent := strings.Join(newLines, "\n") + if !strings.HasSuffix(newContent, "\n") { + newContent += "\n" + } + + return os.WriteFile(envPath, []byte(newContent), 0600) +} + // ============================================================================ // PAT Login Implementation // ============================================================================ @@ -6121,12 +6521,10 @@ func openConfigFile(homeDir string) error { // patLoginModel is the Bubble Tea model for the PAT login UI type patLoginModel struct { textInput textinput.Model - orgInput textinput.Model - status string // "token_input", "org_input", "success", "error" + status string // "token_input", "select_org", "success", "error" errorMsg string username string token string - organization string homeDir string width int height int @@ -6139,7 +6537,6 @@ type ( username string token string } - patOrgSubmittedMsg struct{} ) func newPATLoginModel(homeDir string) patLoginModel { @@ -6153,16 +6550,8 @@ func newPATLoginModel(homeDir string) patLoginModel { ti.EchoCharacter = 'โ€ข' ti.Focus() - oi := textinput.New() - oi.Placeholder = "my-org" - oi.CharLimit = 100 - oi.Width = 30 - oi.Prompt = "> " - oi.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12")) - return patLoginModel{ textInput: ti, - orgInput: oi, status: "token_input", homeDir: homeDir, width: 80, @@ -6210,20 +6599,12 @@ func (m patLoginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Verify token in background return m, verifyPATToken(token) } - if m.status == "org_input" { - m.organization = strings.TrimSpace(m.orgInput.Value()) - return m, func() tea.Msg { return patOrgSubmittedMsg{} } - } } // Pass key messages to textinput if m.status == "token_input" { m.textInput, cmd = m.textInput.Update(msg) return m, cmd } - if m.status == "org_input" { - m.orgInput, cmd = m.orgInput.Update(msg) - return m, cmd - } case tea.WindowSizeMsg: m.width = msg.Width @@ -6231,25 +6612,19 @@ func (m patLoginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.ClearScreen case patTokenVerifiedMsg: - m.status = "org_input" + // Save token and go to org selection m.username = msg.username m.token = msg.token - m.orgInput.Focus() - return m, textinput.Blink - - case patOrgSubmittedMsg: - // Save token and organization to .env - if err := saveTokenToEnv(m.homeDir, m.token, m.organization); err != nil { + // Save token to .env (organization will be set by Select Organization screen) + if err := saveTokenToEnv(m.homeDir, m.token, ""); err != nil { m.status = "error" m.errorMsg = fmt.Sprintf("failed to save token: %v", err) m.done = true return m, nil } - m.status = "success" + m.status = "select_org" m.done = true - return m, tea.Tick(2*time.Second, func(t time.Time) tea.Msg { - return tea.Quit() - }) + return m, tea.Quit case loginErrorMsg: m.status = "error" @@ -6277,8 +6652,6 @@ func (m patLoginModel) View() string { switch m.status { case "token_input": content = m.renderTokenInputView() - case "org_input": - content = m.renderOrgInputView() case "success": content = m.renderSuccessView() case "error": @@ -6326,29 +6699,6 @@ func (m patLoginModel) renderTokenInputView() string { return b.String() } -func (m patLoginModel) renderOrgInputView() string { - var b strings.Builder - - // Calculate spacing for title bar - maxContentWidth := m.width - 4 - if maxContentWidth < 64 { - maxContentWidth = 64 - } - innerWidth := maxContentWidth - 2 - - b.WriteString(renderTitleBar("๐Ÿ”ง Setup", m.username, "", innerWidth) + "\n") - b.WriteString("\n") - b.WriteString(successStyle.Render(fmt.Sprintf("โœ… Successfully authenticated as @%s", m.username)) + "\n") - b.WriteString("\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("\n") - - return b.String() -} - func (m patLoginModel) renderSuccessView() string { var b strings.Builder @@ -6359,18 +6709,13 @@ func (m patLoginModel) renderSuccessView() string { } innerWidth := maxContentWidth - 2 - b.WriteString(renderTitleBar("๐Ÿ”ง Setup", m.username, m.organization, innerWidth) + "\n") + b.WriteString(renderTitleBar("๐Ÿ”ง Setup", m.username, "", innerWidth) + "\n") b.WriteString("\n") - b.WriteString(successStyle.Render("โœ… Setup complete!") + "\n") + b.WriteString(successStyle.Render("โœ… Token saved!") + "\n") b.WriteString("\n") 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("Saved to: %s/.env\n", m.homeDir)) b.WriteString("\n") - b.WriteString("Press any key to continue...\n") - b.WriteString("\n") return b.String() } @@ -6425,6 +6770,13 @@ func RunPATLogin(homeDir string) error { if pm.status == "cancelled" { return nil // Go back without error } + if pm.status == "select_org" { + // Reload .env to pick up the saved token + envPath := homeDir + "/.env" + _ = godotenv.Load(envPath) + // Navigate to Select Organization screen + return runSelectOrgWithFlag(homeDir, pm.username, true) + } if pm.status != "success" { return fmt.Errorf("login cancelled") } diff --git a/main.md b/main.md index 9124426..326827f 100644 --- a/main.md +++ b/main.md @@ -122,12 +122,27 @@ The Setup submenu provides authentication and configuration options: โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` +After login: + +``` +โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ +โ”‚ GitHub Brain / ๐Ÿ”ง Setup ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ +โ”‚ โ”‚ +โ”‚ โ–ถ โœจ Login with device Recommended for organization owners โ”‚ +โ”‚ ๐Ÿ”‘ Login with PAT Works without organization ownership โ”‚ +โ”‚ ๐Ÿข Select organization Choose organization to sync โ”‚ +โ”‚ ๐Ÿ“ Advanced Edit configuration file โ”‚ +โ”‚ โ† Back Esc โ”‚ +โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ +``` + ### Setup Menu Items 1. **โœจ Login with device** - Recommended for organization owners. Runs the OAuth device flow (see [OAuth Login](#oauth-login) section) 2. **๐Ÿ”‘ Login with PAT** - Works without organization ownership. Manually enter a PAT (see [PAT Login](#pat-login) section) -3. **๐Ÿ“ Advanced** - Edit configuration file `{HomeDir}/.env` -4. **โ† Back** - Return to main menu (Esc) +3. **๐Ÿข Select organization** - Choose organization to sync (see [Select Organization](#select-organization) section). Only shown when logged in +4. **๐Ÿ“ Advanced** - Edit configuration file `{HomeDir}/.env` +5. **โ† Back** - Return to main menu (Esc) ### Open Configuration File (Advanced) @@ -260,23 +275,9 @@ The app uses a registered OAuth App for authentication: - `access_denied`: User denied, show error - Success: Returns `access_token` (long-lived, does not expire) -6. On success, prompt for organization: +6. On success, save token to `.env` file and navigate to Select Organization screen (see [Select Organization](#select-organization) section) - ``` - โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ - โ”‚ GitHub ๐Ÿง  Login โ”‚ - โ”‚ โ”‚ - โ”‚ โœ… Successfully authenticated as @wham โ”‚ - โ”‚ โ”‚ - โ”‚ Enter your GitHub organization (optional): โ”‚ - โ”‚ > my-orgโ–ˆ โ”‚ - โ”‚ โ”‚ - โ”‚ Press Enter to skip, or type organization name โ”‚ - โ”‚ โ”‚ - โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ - ``` - -7. Save tokens (and organization if provided) to `.env` file: +7. After organization is selected, show completion screen: ``` โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ @@ -327,11 +328,9 @@ Manual authentication using a Personal Access Token (PAT). Useful when OAuth flo 3. Verify the token by calling `viewer { login }` GraphQL query -4. On success, prompt for organization (same as OAuth flow) +4. On success, save token to `.env` file and navigate to Select Organization screen (see [Select Organization](#select-organization) section) -5. Save token and organization to `.env` file - -6. Return to main menu +5. After organization is selected, show completion screen and return to main menu ### Token Storage @@ -360,6 +359,102 @@ OAuth App tokens are long-lived and do not expire unless revoked. - Timeout: Code expires after `expires_in` seconds (usually 15 minutes) - After saving token, verify it works by fetching `viewer { login }` +## Select Organization + +Allows user to select or change the organization to sync. This screen is accessible from the Setup menu (only shown when logged in) and is also shown automatically after successful login with device or PAT. + +### Organization Selection Flow + +1. On entry, if `GITHUB_TOKEN` is available, fetch user's organizations via GraphQL: + + ```graphql + { + viewer { + organizations(first: 10) { + nodes { + login + } + } + } + } + ``` + +2. Display organization selection screen with inline text input: + + ``` + โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ + โ”‚ GitHub Brain / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ + โ”‚ โ”‚ + โ”‚ โ–ถ my-company โ”‚ + โ”‚ open-source-org โ”‚ + โ”‚ another-org โ”‚ + โ”‚ โ”‚ + โ”‚ Or enter manually: โ–ˆ โ”‚ + โ”‚ โ”‚ + โ”‚ โ†‘โ†“ navigate ยท Enter select ยท type to filter ยท Esc back โ”‚ + โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ + ``` + + When typing in the text input (filters list and allows custom entry): + + ``` + โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ + โ”‚ GitHub Brain / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ + โ”‚ โ”‚ + โ”‚ my-company โ”‚ + โ”‚ โ”‚ + โ”‚ Or enter manually: myโ–ˆ โ”‚ + โ”‚ โ”‚ + โ”‚ โ†‘โ†“ navigate ยท Enter select ยท type to filter ยท Esc back โ”‚ + โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ + ``` + + If no organizations found: + + ``` + โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ + โ”‚ GitHub Brain / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ + โ”‚ โ”‚ + โ”‚ No organizations found โ”‚ + โ”‚ โ”‚ + โ”‚ Or enter manually: โ–ˆ โ”‚ + โ”‚ โ”‚ + โ”‚ Enter organization name ยท Esc back โ”‚ + โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ + ``` + + While loading organizations (with spinner): + + ``` + โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ + โ”‚ GitHub Brain / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ + โ”‚ โ”‚ + โ”‚ โ ‹ Loading organizations... โ”‚ + โ”‚ โ”‚ + โ”‚ Press Esc to cancel โ”‚ + โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ + ``` + +3. On selection (from list or text input): + - Save `ORGANIZATION` to `.env` file + - If accessed from Setup menu, return to Setup menu + - If accessed after login flow, continue to completion screen + +### Menu Navigation + +- Use arrow keys (โ†‘/โ†“) or j/k to navigate organization list +- Typing filters the list and populates the text input +- Press Enter to select highlighted organization, or use text input value if typed +- Press Esc to go back without changing + +### Implementation Notes + +- Query organizations only when screen is entered (not cached) +- Show spinner while loading organizations +- Handle GraphQL errors gracefully - show "Enter custom name" option if query fails +- Use `github.com/charmbracelet/bubbles/textinput` for custom organization input +- Limit to 10 organizations for clean UI display + ## pull Accessed from the main menu. Before starting pull: From 736ffeb9e57bf43d4e0db004416d1b61fa95395c Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:24:26 -0800 Subject: [PATCH 02/11] Enhance organization selection: increase fetch limit to 100, update UI for better navigation and display --- main.go | 59 ++++++++++++++++++++++++++++++++++++--------------------- main.md | 43 ++++++++++++++++++++++++++--------------- 2 files changed, 65 insertions(+), 37 deletions(-) diff --git a/main.go b/main.go index bf3e36b..bab30d0 100644 --- a/main.go +++ b/main.go @@ -6177,7 +6177,7 @@ func fetchOrganizations() tea.Cmd { Nodes []struct { Login string } - } `graphql:"organizations(first: 10)"` + } `graphql:"organizations(first: 100, orderBy: {field: LOGIN, direction: ASC})"` } } @@ -6197,6 +6197,12 @@ func fetchOrganizations() tea.Cmd { func (m selectOrgModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd + // Calculate max display count (10 or fewer) + maxDisplay := len(m.filtered) + if maxDisplay > 10 { + maxDisplay = 10 + } + switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { @@ -6213,7 +6219,7 @@ func (m selectOrgModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.cursor-- } case "down", "ctrl+n": - if m.status == "list" && len(m.filtered) > 0 && m.cursor < len(m.filtered)-1 { + if m.status == "list" && maxDisplay > 0 && m.cursor < maxDisplay-1 { m.cursor++ } case "enter": @@ -6221,8 +6227,8 @@ func (m selectOrgModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var org string inputValue := strings.TrimSpace(m.textInput.Value()) - if len(m.filtered) > 0 && m.cursor < len(m.filtered) { - // Select from filtered list + if maxDisplay > 0 && m.cursor < maxDisplay { + // Select from filtered list (displayed items) org = m.filtered[m.cursor] } else if inputValue != "" { // Use the typed value @@ -6346,11 +6352,16 @@ func (m selectOrgModel) renderLoadingView() string { } innerWidth := maxContentWidth - 2 - b.WriteString(renderTitleBar("๐Ÿข Select organization", m.username, "", innerWidth) + "\n") + b.WriteString(renderTitleBar("๐Ÿ”ง Setup / ๐Ÿข Select organization", m.username, "", innerWidth) + "\n") b.WriteString("\n") b.WriteString(m.spinner.View() + " Loading organizations...\n") b.WriteString("\n") - b.WriteString(dimStyle.Render("Press Esc to cancel") + "\n") + + // Back menu item + selectorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")) + selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")).Bold(true) + paddedName := fmt.Sprintf("%-4s", "Back") + b.WriteString(selectorStyle.Render("โ–ถ") + " โ† " + titleStyle.Render(paddedName) + " " + selectedStyle.Render("Esc")) return b.String() } @@ -6364,19 +6375,22 @@ func (m selectOrgModel) renderListView() string { } innerWidth := maxContentWidth - 2 - b.WriteString(renderTitleBar("๐Ÿข Select organization", m.username, "", innerWidth) + "\n") + b.WriteString(renderTitleBar("๐Ÿ”ง Setup / ๐Ÿข Select organization", m.username, "", innerWidth) + "\n") b.WriteString("\n") selectorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")) selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")).Bold(true) - // Show filtered organizations - if len(m.filtered) == 0 && len(m.organizations) == 0 { + // Show filtered organizations (max 10) + displayOrgs := m.filtered + if len(displayOrgs) > 10 { + displayOrgs = displayOrgs[:10] + } + + if len(m.organizations) == 0 { b.WriteString(dimStyle.Render(" No organizations found") + "\n") - } else if len(m.filtered) == 0 { - b.WriteString(dimStyle.Render(" No matches") + "\n") - } else { - for i, org := range m.filtered { + } else if len(displayOrgs) > 0 { + for i, org := range displayOrgs { cursor := " " style := dimStyle if m.cursor == i { @@ -6390,15 +6404,16 @@ func (m selectOrgModel) renderListView() string { b.WriteString("\n") // Text input for manual entry - b.WriteString(dimStyle.Render(" Or enter manually: ") + m.textInput.View() + "\n") - b.WriteString("\n") - - // Help text - if len(m.filtered) > 0 { - b.WriteString(dimStyle.Render("โ†‘โ†“ navigate ยท Enter select ยท type to filter ยท Esc back") + "\n") + if len(displayOrgs) > 0 { + b.WriteString(dimStyle.Render(" Or enter manually: ") + m.textInput.View() + "\n") } else { - b.WriteString(dimStyle.Render("Enter organization name ยท Esc back") + "\n") + b.WriteString(dimStyle.Render(" Enter manually: ") + m.textInput.View() + "\n") } + b.WriteString("\n") + + // Back menu item - same format as Setup screen + paddedName := fmt.Sprintf("%-4s", "Back") + b.WriteString(selectorStyle.Render("โ–ถ") + " โ† " + titleStyle.Render(paddedName) + " " + selectedStyle.Render("Esc")) return b.String() } @@ -6412,7 +6427,7 @@ func (m selectOrgModel) renderErrorView() string { } innerWidth := maxContentWidth - 2 - b.WriteString(renderTitleBar("๐Ÿข Select organization", m.username, "", innerWidth) + "\n") + b.WriteString(renderTitleBar("๐Ÿ”ง Setup / ๐Ÿข Select organization", m.username, "", innerWidth) + "\n") b.WriteString("\n") b.WriteString(errorStyle.Render("โŒ Error") + "\n") b.WriteString("\n") @@ -6431,7 +6446,7 @@ func (m selectOrgModel) renderSuccessView() string { } innerWidth := maxContentWidth - 2 - b.WriteString(renderTitleBar("๐Ÿข Select organization", m.username, m.selectedOrg, innerWidth) + "\n") + b.WriteString(renderTitleBar("๐Ÿ”ง Setup / ๐Ÿข Select organization", m.username, m.selectedOrg, innerWidth) + "\n") b.WriteString("\n") b.WriteString(successStyle.Render("โœ… Organization saved!") + "\n") b.WriteString("\n") diff --git a/main.md b/main.md index 326827f..21479c9 100644 --- a/main.md +++ b/main.md @@ -365,12 +365,12 @@ Allows user to select or change the organization to sync. This screen is accessi ### Organization Selection Flow -1. On entry, if `GITHUB_TOKEN` is available, fetch user's organizations via GraphQL: +1. On entry, if `GITHUB_TOKEN` is available, fetch user's organizations via GraphQL (max 100, ordered alphabetically): ```graphql { viewer { - organizations(first: 10) { + organizations(first: 100, orderBy: {field: LOGIN, direction: ASC}) { nodes { login } @@ -379,11 +379,11 @@ Allows user to select or change the organization to sync. This screen is accessi } ``` -2. Display organization selection screen with inline text input: +2. Display organization selection screen with inline text input (show first 10 matches): ``` โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ - โ”‚ GitHub Brain / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ + โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ โ”‚ โ”‚ โ”‚ โ–ถ my-company โ”‚ โ”‚ open-source-org โ”‚ @@ -391,21 +391,33 @@ Allows user to select or change the organization to sync. This screen is accessi โ”‚ โ”‚ โ”‚ Or enter manually: โ–ˆ โ”‚ โ”‚ โ”‚ - โ”‚ โ†‘โ†“ navigate ยท Enter select ยท type to filter ยท Esc back โ”‚ + โ”‚ โ–ถ โ† Back Esc โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` - When typing in the text input (filters list and allows custom entry): + When typing in the text input (filters from all 100 orgs, shows top 10 matches): ``` โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ - โ”‚ GitHub Brain / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ + โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ โ”‚ โ”‚ - โ”‚ my-company โ”‚ + โ”‚ โ–ถ my-company โ”‚ โ”‚ โ”‚ โ”‚ Or enter manually: myโ–ˆ โ”‚ โ”‚ โ”‚ - โ”‚ โ†‘โ†“ navigate ยท Enter select ยท type to filter ยท Esc back โ”‚ + โ”‚ โ–ถ โ† Back Esc โ”‚ + โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ + ``` + + When no matches (shows "Enter manually" instead of "Or enter manually"): + + ``` + โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ + โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ + โ”‚ โ”‚ + โ”‚ Enter manually: xyzโ–ˆ โ”‚ + โ”‚ โ”‚ + โ”‚ โ–ถ โ† Back Esc โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` @@ -413,13 +425,13 @@ Allows user to select or change the organization to sync. This screen is accessi ``` โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ - โ”‚ GitHub Brain / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ + โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ โ”‚ โ”‚ โ”‚ No organizations found โ”‚ โ”‚ โ”‚ - โ”‚ Or enter manually: โ–ˆ โ”‚ + โ”‚ Enter manually: โ–ˆ โ”‚ โ”‚ โ”‚ - โ”‚ Enter organization name ยท Esc back โ”‚ + โ”‚ โ–ถ โ† Back Esc โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` @@ -427,11 +439,11 @@ Allows user to select or change the organization to sync. This screen is accessi ``` โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ - โ”‚ GitHub Brain / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ + โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ โ”‚ โ”‚ โ”‚ โ ‹ Loading organizations... โ”‚ โ”‚ โ”‚ - โ”‚ Press Esc to cancel โ”‚ + โ”‚ โ–ถ โ† Back Esc โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` @@ -442,7 +454,8 @@ Allows user to select or change the organization to sync. This screen is accessi ### Menu Navigation -- Use arrow keys (โ†‘/โ†“) or j/k to navigate organization list +- Use arrow keys (โ†‘/โ†“) to navigate organization list (max 10 displayed) +- Typing filters from all organizations (up to 100), shows top 10 matches - Typing filters the list and populates the text input - Press Enter to select highlighted organization, or use text input value if typed - Press Esc to go back without changing From 157a2dfb21939b4de23350ea07795b89d87eb45e Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:30:28 -0800 Subject: [PATCH 03/11] Fix formatting in GraphQL query for organization selection --- main.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.md b/main.md index 21479c9..5a1303d 100644 --- a/main.md +++ b/main.md @@ -370,7 +370,7 @@ Allows user to select or change the organization to sync. This screen is accessi ```graphql { viewer { - organizations(first: 100, orderBy: {field: LOGIN, direction: ASC}) { + organizations(first: 100, orderBy: { field: LOGIN, direction: ASC }) { nodes { login } From 0a36ec225b9a8b73245d52e8da6be9a672798424 Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:30:29 -0800 Subject: [PATCH 04/11] YOLO --- main.go | 73 +++++++++++++++++++++++++++++++++++++++++++-------------- main.md | 33 +++++++++++++++++++------- 2 files changed, 81 insertions(+), 25 deletions(-) diff --git a/main.go b/main.go index bab30d0..948a8d3 100644 --- a/main.go +++ b/main.go @@ -6202,6 +6202,12 @@ func (m selectOrgModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if maxDisplay > 10 { maxDisplay = 10 } + + // Menu items: [orgs...] [enter manually] [back] + inputIndex := maxDisplay + backIndex := inputIndex + 1 + isInputSelected := m.cursor == inputIndex + isBackSelected := m.cursor == backIndex switch msg := msg.(type) { case tea.KeyMsg: @@ -6219,20 +6225,27 @@ func (m selectOrgModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.cursor-- } case "down", "ctrl+n": - if m.status == "list" && maxDisplay > 0 && m.cursor < maxDisplay-1 { + // Can navigate down to backIndex + if m.status == "list" && m.cursor < backIndex { m.cursor++ } case "enter": if m.status == "list" { + if isBackSelected { + // Go back + m.status = "cancelled" + m.done = true + return m, tea.Quit + } + var org string - inputValue := strings.TrimSpace(m.textInput.Value()) - if maxDisplay > 0 && m.cursor < maxDisplay { + if isInputSelected { + // Use the typed value from text input + org = strings.TrimSpace(m.textInput.Value()) + } else if m.cursor < maxDisplay { // Select from filtered list (displayed items) org = m.filtered[m.cursor] - } else if inputValue != "" { - // Use the typed value - org = inputValue } if org != "" { @@ -6242,15 +6255,20 @@ func (m selectOrgModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - // Pass other key messages to textinput when in list mode - if m.status == "list" { + // Pass key messages to textinput only when the input is selected + if m.status == "list" && isInputSelected { prevValue := m.textInput.Value() m.textInput, cmd = m.textInput.Update(msg) - // If text changed, update filtered list and reset cursor + // If text changed, update filtered list (but keep cursor on input) if m.textInput.Value() != prevValue { m.filtered = m.filterOrganizations(m.textInput.Value()) - m.cursor = 0 + // Recalculate inputIndex and keep cursor there + newMaxDisplay := len(m.filtered) + if newMaxDisplay > 10 { + newMaxDisplay = 10 + } + m.cursor = newMaxDisplay } return m, cmd } @@ -6387,6 +6405,10 @@ func (m selectOrgModel) renderListView() string { displayOrgs = displayOrgs[:10] } + // The "enter manually" input is at index len(displayOrgs) + inputIndex := len(displayOrgs) + isInputSelected := m.cursor == inputIndex + if len(m.organizations) == 0 { b.WriteString(dimStyle.Render(" No organizations found") + "\n") } else if len(displayOrgs) > 0 { @@ -6403,17 +6425,34 @@ func (m selectOrgModel) renderListView() string { b.WriteString("\n") - // Text input for manual entry - if len(displayOrgs) > 0 { - b.WriteString(dimStyle.Render(" Or enter manually: ") + m.textInput.View() + "\n") + // Text input for manual entry (as a selectable item) + label := "Or enter manually: " + if len(displayOrgs) == 0 { + label = "Enter manually: " + } + + if isInputSelected { + // Input is selected - show selector and active input + b.WriteString(selectorStyle.Render("โ–ถ") + " " + dimStyle.Render(label) + m.textInput.View() + "\n") } else { - b.WriteString(dimStyle.Render(" Enter manually: ") + m.textInput.View() + "\n") + // Input is not selected - show dimmed + inputValue := m.textInput.Value() + if inputValue == "" { + b.WriteString(" " + dimStyle.Render(label) + "\n") + } else { + b.WriteString(" " + dimStyle.Render(label) + inputValue + "\n") + } } b.WriteString("\n") - // Back menu item - same format as Setup screen - paddedName := fmt.Sprintf("%-4s", "Back") - b.WriteString(selectorStyle.Render("โ–ถ") + " โ† " + titleStyle.Render(paddedName) + " " + selectedStyle.Render("Esc")) + // Back menu item (selectable) + backIndex := inputIndex + 1 + isBackSelected := m.cursor == backIndex + if isBackSelected { + b.WriteString(selectorStyle.Render("โ–ถ") + " " + dimStyle.Render("โ†") + " " + selectedStyle.Render("Back")) + } else { + b.WriteString(" " + dimStyle.Render("โ†") + " " + dimStyle.Render("Back")) + } return b.String() } diff --git a/main.md b/main.md index 5a1303d..6fdf89c 100644 --- a/main.md +++ b/main.md @@ -379,7 +379,7 @@ Allows user to select or change the organization to sync. This screen is accessi } ``` -2. Display organization selection screen with inline text input (show first 10 matches): +2. Display organization selection screen with selectable list and text input (show first 10 matches): ``` โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ @@ -389,7 +389,23 @@ Allows user to select or change the organization to sync. This screen is accessi โ”‚ open-source-org โ”‚ โ”‚ another-org โ”‚ โ”‚ โ”‚ - โ”‚ Or enter manually: โ–ˆ โ”‚ + โ”‚ Or enter manually: โ”‚ + โ”‚ โ”‚ + โ”‚ โ–ถ โ† Back Esc โ”‚ + โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ + ``` + + When "Enter manually" is selected (navigate down past the list): + + ``` + โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ + โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ + โ”‚ โ”‚ + โ”‚ my-company โ”‚ + โ”‚ open-source-org โ”‚ + โ”‚ another-org โ”‚ + โ”‚ โ”‚ + โ”‚ โ–ถ Or enter manually: โ–ˆ โ”‚ โ”‚ โ”‚ โ”‚ โ–ถ โ† Back Esc โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ @@ -401,9 +417,9 @@ Allows user to select or change the organization to sync. This screen is accessi โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ โ”‚ โ”‚ - โ”‚ โ–ถ my-company โ”‚ + โ”‚ my-company โ”‚ โ”‚ โ”‚ - โ”‚ Or enter manually: myโ–ˆ โ”‚ + โ”‚ โ–ถ Or enter manually: myโ–ˆ โ”‚ โ”‚ โ”‚ โ”‚ โ–ถ โ† Back Esc โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ @@ -415,7 +431,7 @@ Allows user to select or change the organization to sync. This screen is accessi โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿข Select organization ๐Ÿ‘ค @wham ยท 1.0.0 โ”‚ โ”‚ โ”‚ - โ”‚ Enter manually: xyzโ–ˆ โ”‚ + โ”‚ โ–ถ Enter manually: xyzโ–ˆ โ”‚ โ”‚ โ”‚ โ”‚ โ–ถ โ† Back Esc โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ @@ -429,7 +445,7 @@ Allows user to select or change the organization to sync. This screen is accessi โ”‚ โ”‚ โ”‚ No organizations found โ”‚ โ”‚ โ”‚ - โ”‚ Enter manually: โ–ˆ โ”‚ + โ”‚ โ–ถ Enter manually: โ–ˆ โ”‚ โ”‚ โ”‚ โ”‚ โ–ถ โ† Back Esc โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ @@ -455,9 +471,10 @@ Allows user to select or change the organization to sync. This screen is accessi ### Menu Navigation - Use arrow keys (โ†‘/โ†“) to navigate organization list (max 10 displayed) +- Navigate down past the list to select "Enter manually" option +- Typing only works when "Enter manually" is selected - Typing filters from all organizations (up to 100), shows top 10 matches -- Typing filters the list and populates the text input -- Press Enter to select highlighted organization, or use text input value if typed +- Press Enter to select highlighted organization or submit manual entry - Press Esc to go back without changing ### Implementation Notes From 20f3bc8480d99894f0f7df29908795b36a13e7db Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:45:46 -0800 Subject: [PATCH 05/11] Refactor environment variable loading to use Overload method for better handling of .env file --- main.go | 73 +++++++++++++++++++++------------------------------------ 1 file changed, 27 insertions(+), 46 deletions(-) diff --git a/main.go b/main.go index 948a8d3..9e33944 100644 --- a/main.go +++ b/main.go @@ -4285,7 +4285,7 @@ func main() { // Load environment variables from home directory envPath := homeDir + "/.env" - _ = godotenv.Load(envPath) + _ = godotenv.Overload(envPath) // Check if a command is specified cmd := "" @@ -4982,7 +4982,8 @@ func (m mainMenuModel) Init() tea.Cmd { func checkAuthCmd(homeDir string) tea.Cmd { return func() tea.Msg { - // Check if we have a token + // Load .env file + _ = godotenv.Overload(homeDir + "/.env") token := os.Getenv("GITHUB_TOKEN") if token == "" { return authCheckResultMsg{loggedIn: false} @@ -5003,11 +5004,10 @@ func checkAuthCmd(homeDir string) tea.Cmd { return authCheckResultMsg{loggedIn: false} } - org := os.Getenv("ORGANIZATION") return authCheckResultMsg{ loggedIn: true, username: query.Viewer.Login, - organization: org, + organization: os.Getenv("ORGANIZATION"), } } } @@ -5145,9 +5145,6 @@ func RunMainTUI(homeDir string) error { // Log error but continue to menu slog.Error("Setup failed", "error", err) } - // Reload .env after setup - envPath := homeDir + "/.env" - _ = godotenv.Load(envPath) continue } @@ -5156,9 +5153,6 @@ func RunMainTUI(homeDir string) error { // Error already handled in runPullOperation slog.Error("Pull failed", "error", err) } - // Reload .env after pull (in case organization was set) - envPath := homeDir + "/.env" - _ = godotenv.Load(envPath) continue } } @@ -5166,7 +5160,8 @@ func RunMainTUI(homeDir string) error { // runPullOperation runs the pull operation from the TUI func runPullOperation(homeDir, username, org string) error { - // Check for token + // Load .env file + _ = godotenv.Overload(homeDir + "/.env") token := os.Getenv("GITHUB_TOKEN") if token == "" { // Need to prompt for login first @@ -5191,11 +5186,8 @@ func runPullOperation(homeDir, username, org string) error { if err := saveOrganizationToEnv(homeDir, organization); err != nil { return fmt.Errorf("failed to save organization: %w", err) } - // Reload env - envPath := homeDir + "/.env" - _ = godotenv.Load(envPath) } - + // Build config config := &Config{ Organization: organization, @@ -5832,9 +5824,6 @@ func RunLogin(homeDir, currentUsername, currentOrg string) error { return nil // Go back without error } if lm.status == "select_org" { - // Reload .env to pick up the saved token - envPath := homeDir + "/.env" - _ = godotenv.Load(envPath) // Navigate to Select Organization screen return runSelectOrgWithFlag(homeDir, lm.username, true) } @@ -6036,9 +6025,6 @@ func RunSetupMenu(homeDir, username, organization string) error { } slog.Error("OAuth login failed", "error", err) } - // Reload .env after login - envPath := homeDir + "/.env" - _ = godotenv.Load(envPath) return nil // Return to main menu after login } @@ -6049,9 +6035,6 @@ func RunSetupMenu(homeDir, username, organization string) error { } slog.Error("PAT login failed", "error", err) } - // Reload .env after login - envPath := homeDir + "/.env" - _ = godotenv.Load(envPath) return nil // Return to main menu after login } @@ -6062,10 +6045,10 @@ func RunSetupMenu(homeDir, username, organization string) error { } slog.Error("Select organization failed", "error", err) } - // Reload .env after selection - envPath := homeDir + "/.env" - _ = godotenv.Load(envPath) - return nil // Return to main menu after selection + // Re-read organization from .env file + _ = godotenv.Overload(homeDir + "/.env") + organization = os.Getenv("ORGANIZATION") + continue // Return to Setup menu } if sm.openConfig { @@ -6156,12 +6139,13 @@ func (m selectOrgModel) Init() tea.Cmd { return tea.Batch( m.spinner.Tick, textinput.Blink, - fetchOrganizations(), + fetchOrganizations(m.homeDir), ) } -func fetchOrganizations() tea.Cmd { +func fetchOrganizations(homeDir string) tea.Cmd { return func() tea.Msg { + _ = godotenv.Overload(homeDir + "/.env") token := os.Getenv("GITHUB_TOKEN") if token == "" { return orgsLoadErrorMsg{err: fmt.Errorf("no GitHub token found")} @@ -6414,44 +6398,44 @@ func (m selectOrgModel) renderListView() string { } else if len(displayOrgs) > 0 { for i, org := range displayOrgs { cursor := " " - style := dimStyle + nameStyle := titleStyle // Bold for main item name if m.cursor == i { cursor = selectorStyle.Render("โ–ถ") + " " - style = selectedStyle + nameStyle = selectedStyle // Blue bold when selected } - b.WriteString(cursor + style.Render(org) + "\n") + b.WriteString(cursor + nameStyle.Render(org) + "\n") } } b.WriteString("\n") // Text input for manual entry (as a selectable item) - label := "Or enter manually: " + label := "Or enter manually" if len(displayOrgs) == 0 { - label = "Enter manually: " + label = "Enter manually" } if isInputSelected { - // Input is selected - show selector and active input - b.WriteString(selectorStyle.Render("โ–ถ") + " " + dimStyle.Render(label) + m.textInput.View() + "\n") + // Input is selected - show selector, bold label, and active input + b.WriteString(selectorStyle.Render("โ–ถ") + " " + selectedStyle.Render(label) + " " + m.textInput.View() + "\n") } else { - // Input is not selected - show dimmed + // Input is not selected - show dimmed label inputValue := m.textInput.Value() if inputValue == "" { - b.WriteString(" " + dimStyle.Render(label) + "\n") + b.WriteString(" " + titleStyle.Render(label) + "\n") } else { - b.WriteString(" " + dimStyle.Render(label) + inputValue + "\n") + b.WriteString(" " + titleStyle.Render(label) + " " + dimStyle.Render(inputValue) + "\n") } } b.WriteString("\n") - // Back menu item (selectable) + // Back menu item (selectable) - styled like Setup menu backIndex := inputIndex + 1 isBackSelected := m.cursor == backIndex if isBackSelected { - b.WriteString(selectorStyle.Render("โ–ถ") + " " + dimStyle.Render("โ†") + " " + selectedStyle.Render("Back")) + b.WriteString(selectorStyle.Render("โ–ถ") + " โ† " + selectedStyle.Render("Back") + " " + selectedStyle.Render("Esc")) } else { - b.WriteString(" " + dimStyle.Render("โ†") + " " + dimStyle.Render("Back")) + b.WriteString(" โ† " + titleStyle.Render("Back") + " " + dimStyle.Render("Esc")) } return b.String() @@ -6825,9 +6809,6 @@ func RunPATLogin(homeDir string) error { return nil // Go back without error } if pm.status == "select_org" { - // Reload .env to pick up the saved token - envPath := homeDir + "/.env" - _ = godotenv.Load(envPath) // Navigate to Select Organization screen return runSelectOrgWithFlag(homeDir, pm.username, true) } From c43b3397c4f42aa72820957e0f01a10ff0c66518 Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Wed, 31 Dec 2025 22:11:26 -0800 Subject: [PATCH 06/11] Refactor PAT login flow: update function signatures and enhance UI for better organization context --- main.go | 69 +++++++++++++++++++++++++++++++-------------------------- main.md | 49 ++++++++++++++++++++-------------------- 2 files changed, 62 insertions(+), 56 deletions(-) diff --git a/main.go b/main.go index 9e33944..400c74f 100644 --- a/main.go +++ b/main.go @@ -6029,7 +6029,7 @@ func RunSetupMenu(homeDir, username, organization string) error { } if sm.runPAT { - if err := RunPATLogin(homeDir); err != nil { + if err := RunPATLogin(homeDir, username, organization); err != nil { if err.Error() == "quit" { return err // Propagate quit to exit app } @@ -6558,15 +6558,17 @@ func saveOrgToEnv(homeDir string, organization string) error { // patLoginModel is the Bubble Tea model for the PAT login UI type patLoginModel struct { - textInput textinput.Model - status string // "token_input", "select_org", "success", "error" - errorMsg string - username string - token string - homeDir string - width int - height int - done bool + textInput textinput.Model + status string // "token_input", "select_org", "success", "error" + errorMsg string + username string + token string + homeDir string + width int + height int + done bool + currentUsername string // current logged-in username for title bar + currentOrg string // current organization for title bar } // PAT login message types @@ -6577,7 +6579,7 @@ type ( } ) -func newPATLoginModel(homeDir string) patLoginModel { +func newPATLoginModel(homeDir, currentUsername, currentOrg string) patLoginModel { ti := textinput.New() ti.Placeholder = "github_pat_..." ti.CharLimit = 200 @@ -6589,11 +6591,13 @@ func newPATLoginModel(homeDir string) patLoginModel { ti.Focus() return patLoginModel{ - textInput: ti, - status: "token_input", - homeDir: homeDir, - width: 80, - height: 24, + textInput: ti, + status: "token_input", + homeDir: homeDir, + width: 80, + height: 24, + currentUsername: currentUsername, + currentOrg: currentOrg, } } @@ -6722,17 +6726,17 @@ func (m patLoginModel) renderTokenInputView() string { } innerWidth := maxContentWidth - 2 - b.WriteString(renderTitleBar("๐Ÿ”ง Setup", "", "", innerWidth) + "\n") + b.WriteString(renderTitleBar("๐Ÿ”ง Setup / ๐Ÿ”‘ Login with PAT", m.currentUsername, m.currentOrg, innerWidth) + "\n") b.WriteString("\n") - b.WriteString("๐Ÿ”‘ Personal Access Token\n") + b.WriteString(" 1. Opening browser to create a new token at github.com\n") b.WriteString("\n") - b.WriteString("1. Create a token at github.com (opened in browser)\n") + b.WriteString(" 2. Paste your token here:\n") b.WriteString("\n") - b.WriteString("2. Paste your token here:\n") - b.WriteString(m.textInput.View() + "\n") + b.WriteString(" " + m.textInput.View() + "\n") b.WriteString("\n") - b.WriteString(dimStyle.Render("Press Enter to continue, Esc to cancel") + "\n") + b.WriteString(" Press Enter to continue\n") b.WriteString("\n") + b.WriteString(" " + dimStyle.Render("โ† Back Esc") + "\n") return b.String() } @@ -6747,12 +6751,12 @@ func (m patLoginModel) renderSuccessView() string { } innerWidth := maxContentWidth - 2 - b.WriteString(renderTitleBar("๐Ÿ”ง Setup", m.username, "", innerWidth) + "\n") + b.WriteString(renderTitleBar("๐Ÿ”ง Setup / ๐Ÿ”‘ Login with PAT", m.username, "", innerWidth) + "\n") b.WriteString("\n") - b.WriteString(successStyle.Render("โœ… Token saved!") + "\n") + b.WriteString(" " + successStyle.Render("โœ… Token saved!") + "\n") b.WriteString("\n") - b.WriteString(fmt.Sprintf("Logged in as: @%s\n", m.username)) - b.WriteString(fmt.Sprintf("Saved to: %s/.env\n", m.homeDir)) + b.WriteString(fmt.Sprintf(" Logged in as: @%s\n", m.username)) + b.WriteString(fmt.Sprintf(" Saved to: %s/.env\n", m.homeDir)) b.WriteString("\n") return b.String() @@ -6768,27 +6772,28 @@ func (m patLoginModel) renderErrorView() string { } innerWidth := maxContentWidth - 2 - b.WriteString(renderTitleBar("๐Ÿ”ง Setup", "", "", innerWidth) + "\n") + b.WriteString(renderTitleBar("๐Ÿ”ง Setup / ๐Ÿ”‘ Login with PAT", m.currentUsername, m.currentOrg, innerWidth) + "\n") b.WriteString("\n") - b.WriteString(errorStyle.Render("โŒ Authentication failed") + "\n") + b.WriteString(" " + errorStyle.Render("โŒ Authentication failed") + "\n") b.WriteString("\n") - b.WriteString(fmt.Sprintf("Error: %s\n", m.errorMsg)) + b.WriteString(fmt.Sprintf(" Error: %s\n", m.errorMsg)) b.WriteString("\n") - b.WriteString("Please try again.\n") + b.WriteString(" Please try again.\n") b.WriteString("\n") + b.WriteString(" " + dimStyle.Render("โ† Back Esc") + "\n") return b.String() } // RunPATLogin runs the PAT login flow -func RunPATLogin(homeDir string) error { +func RunPATLogin(homeDir, currentUsername, currentOrg string) error { // Ensure home directory exists if err := os.MkdirAll(homeDir, 0755); err != nil { return fmt.Errorf("failed to create home directory: %w", err) } // Create the Bubble Tea model - m := newPATLoginModel(homeDir) + m := newPATLoginModel(homeDir, currentUsername, currentOrg) p := tea.NewProgram(m, tea.WithAltScreen()) // Run the Bubble Tea program diff --git a/main.md b/main.md index 6fdf89c..026130c 100644 --- a/main.md +++ b/main.md @@ -277,24 +277,7 @@ The app uses a registered OAuth App for authentication: 6. On success, save token to `.env` file and navigate to Select Organization screen (see [Select Organization](#select-organization) section) -7. After organization is selected, show completion screen: - - ``` - โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ - โ”‚ GitHub ๐Ÿง  Login โ”‚ - โ”‚ โ”‚ - โ”‚ โœ… Setup complete! โ”‚ - โ”‚ โ”‚ - โ”‚ Logged in as: @wham โ”‚ - โ”‚ Organization: my-org โ”‚ - โ”‚ Saved to: ~/.github-brain/.env โ”‚ - โ”‚ โ”‚ - โ”‚ Press any key to continue... โ”‚ - โ”‚ โ”‚ - โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ - ``` - -8. Return to main menu after key press. +7. After organization is selected, return to Setup menu ## PAT Login @@ -312,17 +295,35 @@ Manual authentication using a Personal Access Token (PAT). Useful when OAuth flo ``` โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ - โ”‚ GitHub ๐Ÿง  Login โ”‚ + โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿ”‘ Login with PAT 1.0.0 โ”‚ + โ”‚ โ”‚ + โ”‚ 1. Opening browser to create a new token at github.com โ”‚ โ”‚ โ”‚ - โ”‚ ๐Ÿ”‘ Personal Access Token โ”‚ + โ”‚ 2. Paste your token here: โ”‚ โ”‚ โ”‚ - โ”‚ 1. Create a token at github.com (opened in browser) โ”‚ + โ”‚ > github_pat_โ–ˆ โ”‚ + โ”‚ โ”‚ + โ”‚ Press Enter to continue โ”‚ + โ”‚ โ”‚ + โ”‚ โ† Back Esc โ”‚ + โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ + ``` + + With user logged in (and organization configured): + + ``` + โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ + โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿ”‘ Login with PAT ๐Ÿ‘ค @wham ยท ๐Ÿข my-org ยท 1.0.0 โ”‚ + โ”‚ โ”‚ + โ”‚ 1. Opening browser to create a new token at github.com โ”‚ โ”‚ โ”‚ โ”‚ 2. Paste your token here: โ”‚ - โ”‚ > github_pat_โ–ˆ โ”‚ โ”‚ โ”‚ - โ”‚ Press Enter to continue, Esc to cancel โ”‚ + โ”‚ > github_pat_โ–ˆ โ”‚ + โ”‚ โ”‚ + โ”‚ Press Enter to continue โ”‚ โ”‚ โ”‚ + โ”‚ โ† Back Esc โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` @@ -330,7 +331,7 @@ Manual authentication using a Personal Access Token (PAT). Useful when OAuth flo 4. On success, save token to `.env` file and navigate to Select Organization screen (see [Select Organization](#select-organization) section) -5. After organization is selected, show completion screen and return to main menu +5. After organization is selected, return to Setup menu ### Token Storage From eed61e687764e03beb7ea88e501b68433fab312b Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Wed, 31 Dec 2025 22:19:43 -0800 Subject: [PATCH 07/11] Enhance PAT login UI: update prompts for clarity and improve user navigation with cursor selection --- main.go | 60 +++++++++++++++++++++++++++++++++++++++++++++------------ main.md | 22 ++++++++++----------- 2 files changed, 58 insertions(+), 24 deletions(-) diff --git a/main.go b/main.go index 400c74f..9e88e45 100644 --- a/main.go +++ b/main.go @@ -6569,6 +6569,7 @@ type patLoginModel struct { done bool currentUsername string // current logged-in username for title bar currentOrg string // current organization for title bar + cursor int // 0 = paste input, 1 = back } // PAT login message types @@ -6581,11 +6582,10 @@ type ( func newPATLoginModel(homeDir, currentUsername, currentOrg string) patLoginModel { ti := textinput.New() - ti.Placeholder = "github_pat_..." + ti.Placeholder = "" ti.CharLimit = 200 - ti.Width = 50 - ti.Prompt = "> " - ti.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12")) + ti.Width = 40 + ti.Prompt = "" ti.EchoMode = textinput.EchoPassword ti.EchoCharacter = 'โ€ข' ti.Focus() @@ -6598,6 +6598,7 @@ func newPATLoginModel(homeDir, currentUsername, currentOrg string) patLoginModel height: 24, currentUsername: currentUsername, currentOrg: currentOrg, + cursor: 0, // Start with paste input selected } } @@ -6631,8 +6632,31 @@ func (m patLoginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.status = "cancelled" m.done = true return m, tea.Quit + case "up", "k": + if m.status == "token_input" && m.cursor > 0 { + m.cursor-- + if m.cursor == 0 { + m.textInput.Focus() + } + } + return m, nil + case "down", "j": + if m.status == "token_input" && m.cursor < 1 { + m.cursor++ + if m.cursor == 1 { + m.textInput.Blur() + } + } + return m, nil case "enter": if m.status == "token_input" { + if m.cursor == 1 { + // Back selected + m.status = "cancelled" + m.done = true + return m, tea.Quit + } + // Paste input selected token := strings.TrimSpace(m.textInput.Value()) if token == "" { return m, nil @@ -6642,8 +6666,8 @@ func (m patLoginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, verifyPATToken(token) } } - // Pass key messages to textinput - if m.status == "token_input" { + // Pass key messages to textinput only when paste input is selected + if m.status == "token_input" && m.cursor == 0 { m.textInput, cmd = m.textInput.Update(msg) return m, cmd } @@ -6725,18 +6749,30 @@ func (m patLoginModel) renderTokenInputView() string { maxContentWidth = 64 } innerWidth := maxContentWidth - 2 + selectorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")) b.WriteString(renderTitleBar("๐Ÿ”ง Setup / ๐Ÿ”‘ Login with PAT", m.currentUsername, m.currentOrg, innerWidth) + "\n") b.WriteString("\n") - b.WriteString(" 1. Opening browser to create a new token at github.com\n") - b.WriteString("\n") - b.WriteString(" 2. Paste your token here:\n") + b.WriteString(" 1. Opening browser to create new PAT (personal access token)\n") + b.WriteString(" at https://github.com/settings/personal-access-tokens/new\n") b.WriteString("\n") - b.WriteString(" " + m.textInput.View() + "\n") + b.WriteString(" 2. Copy the PAT\n") b.WriteString("\n") - b.WriteString(" Press Enter to continue\n") + + // Paste option + if m.cursor == 0 { + b.WriteString(selectorStyle.Render("โ–ถ") + " Paste the PAT and press Enter: " + m.textInput.View() + "\n") + } else { + b.WriteString(" Paste the PAT and press Enter: " + m.textInput.View() + "\n") + } b.WriteString("\n") - b.WriteString(" " + dimStyle.Render("โ† Back Esc") + "\n") + + // Back option + if m.cursor == 1 { + b.WriteString(selectorStyle.Render("โ–ถ") + " " + dimStyle.Render("โ† Back Esc") + "\n") + } else { + b.WriteString(" " + dimStyle.Render("โ† Back Esc") + "\n") + } return b.String() } diff --git a/main.md b/main.md index 026130c..93dae7a 100644 --- a/main.md +++ b/main.md @@ -297,15 +297,14 @@ Manual authentication using a Personal Access Token (PAT). Useful when OAuth flo โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿ”‘ Login with PAT 1.0.0 โ”‚ โ”‚ โ”‚ - โ”‚ 1. Opening browser to create a new token at github.com โ”‚ + โ”‚ 1. Opening browser to create new PAT (personal access token) โ”‚ + โ”‚ at https://github.com/settings/personal-access-tokens/new โ”‚ โ”‚ โ”‚ - โ”‚ 2. Paste your token here: โ”‚ + โ”‚ 2. Copy the PAT โ”‚ โ”‚ โ”‚ - โ”‚ > github_pat_โ–ˆ โ”‚ + โ”‚ โ–ถ Paste the PAT and press Enter: โ–ˆ โ”‚ โ”‚ โ”‚ - โ”‚ Press Enter to continue โ”‚ - โ”‚ โ”‚ - โ”‚ โ† Back Esc โ”‚ + โ”‚ โ† Back Esc โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` @@ -315,15 +314,14 @@ Manual authentication using a Personal Access Token (PAT). Useful when OAuth flo โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿ”‘ Login with PAT ๐Ÿ‘ค @wham ยท ๐Ÿข my-org ยท 1.0.0 โ”‚ โ”‚ โ”‚ - โ”‚ 1. Opening browser to create a new token at github.com โ”‚ - โ”‚ โ”‚ - โ”‚ 2. Paste your token here: โ”‚ + โ”‚ 1. Opening browser to create new PAT (personal access token) โ”‚ + โ”‚ at https://github.com/settings/personal-access-tokens/new โ”‚ โ”‚ โ”‚ - โ”‚ > github_pat_โ–ˆ โ”‚ + โ”‚ 2. Copy the PAT โ”‚ โ”‚ โ”‚ - โ”‚ Press Enter to continue โ”‚ + โ”‚ โ–ถ Paste the PAT and press Enter: โ–ˆ โ”‚ โ”‚ โ”‚ - โ”‚ โ† Back Esc โ”‚ + โ”‚ โ† Back Esc โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` From c5489e945828c1c5017923cdb93c255add2e9ccb Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Wed, 31 Dec 2025 22:33:22 -0800 Subject: [PATCH 08/11] Improve PAT login UI: standardize spacing and enhance back navigation styling for clarity --- main.go | 23 ++++++++++++----------- main.md | 18 ++++++++---------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/main.go b/main.go index 9e88e45..0dbdc17 100644 --- a/main.go +++ b/main.go @@ -6429,11 +6429,11 @@ func (m selectOrgModel) renderListView() string { } b.WriteString("\n") - // Back menu item (selectable) - styled like Setup menu + // Back menu item (selectable) - styled like Setup menu (name always bold, description changes) backIndex := inputIndex + 1 isBackSelected := m.cursor == backIndex if isBackSelected { - b.WriteString(selectorStyle.Render("โ–ถ") + " โ† " + selectedStyle.Render("Back") + " " + selectedStyle.Render("Esc")) + b.WriteString(selectorStyle.Render("โ–ถ") + " โ† " + titleStyle.Render("Back") + " " + selectedStyle.Render("Esc")) } else { b.WriteString(" โ† " + titleStyle.Render("Back") + " " + dimStyle.Render("Esc")) } @@ -6750,28 +6750,29 @@ func (m patLoginModel) renderTokenInputView() string { } innerWidth := maxContentWidth - 2 selectorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")) + selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("12")).Bold(true) + titleStyle := lipgloss.NewStyle().Bold(true) b.WriteString(renderTitleBar("๐Ÿ”ง Setup / ๐Ÿ”‘ Login with PAT", m.currentUsername, m.currentOrg, innerWidth) + "\n") b.WriteString("\n") - b.WriteString(" 1. Opening browser to create new PAT (personal access token)\n") - b.WriteString(" at https://github.com/settings/personal-access-tokens/new\n") + b.WriteString(" 1. Opening browser to create new PAT at github.com\n") b.WriteString("\n") - b.WriteString(" 2. Copy the PAT\n") + b.WriteString(" 2. Copy the PAT\n") b.WriteString("\n") // Paste option if m.cursor == 0 { - b.WriteString(selectorStyle.Render("โ–ถ") + " Paste the PAT and press Enter: " + m.textInput.View() + "\n") + b.WriteString(selectorStyle.Render("โ–ถ") + " Paste the PAT and press Enter: " + m.textInput.View() + "\n") } else { - b.WriteString(" Paste the PAT and press Enter: " + m.textInput.View() + "\n") + b.WriteString(" Paste the PAT and press Enter: " + m.textInput.View() + "\n") } b.WriteString("\n") - // Back option + // Back option - styled like Setup menu (name always bold, description changes) if m.cursor == 1 { - b.WriteString(selectorStyle.Render("โ–ถ") + " " + dimStyle.Render("โ† Back Esc") + "\n") + b.WriteString(selectorStyle.Render("โ–ถ") + " โ† " + titleStyle.Render("Back") + " " + selectedStyle.Render("Esc") + "\n") } else { - b.WriteString(" " + dimStyle.Render("โ† Back Esc") + "\n") + b.WriteString(" โ† " + titleStyle.Render("Back") + " " + dimStyle.Render("Esc") + "\n") } return b.String() @@ -6816,7 +6817,7 @@ func (m patLoginModel) renderErrorView() string { b.WriteString("\n") b.WriteString(" Please try again.\n") b.WriteString("\n") - b.WriteString(" " + dimStyle.Render("โ† Back Esc") + "\n") + b.WriteString(" โ† " + titleStyle.Render("Back") + " " + dimStyle.Render("Esc") + "\n") return b.String() } diff --git a/main.md b/main.md index 93dae7a..7952139 100644 --- a/main.md +++ b/main.md @@ -297,14 +297,13 @@ Manual authentication using a Personal Access Token (PAT). Useful when OAuth flo โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿ”‘ Login with PAT 1.0.0 โ”‚ โ”‚ โ”‚ - โ”‚ 1. Opening browser to create new PAT (personal access token) โ”‚ - โ”‚ at https://github.com/settings/personal-access-tokens/new โ”‚ + โ”‚ 1. Opening browser to create new PAT at github.com โ”‚ โ”‚ โ”‚ - โ”‚ 2. Copy the PAT โ”‚ + โ”‚ 2. Copy the PAT โ”‚ โ”‚ โ”‚ - โ”‚ โ–ถ Paste the PAT and press Enter: โ–ˆ โ”‚ + โ”‚ โ–ถ Paste the PAT and press Enter: โ–ˆ โ”‚ โ”‚ โ”‚ - โ”‚ โ† Back Esc โ”‚ + โ”‚ โ† Back Esc โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` @@ -314,14 +313,13 @@ Manual authentication using a Personal Access Token (PAT). Useful when OAuth flo โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿ”‘ Login with PAT ๐Ÿ‘ค @wham ยท ๐Ÿข my-org ยท 1.0.0 โ”‚ โ”‚ โ”‚ - โ”‚ 1. Opening browser to create new PAT (personal access token) โ”‚ - โ”‚ at https://github.com/settings/personal-access-tokens/new โ”‚ + โ”‚ 1. Opening browser to create new PAT at github.com โ”‚ โ”‚ โ”‚ - โ”‚ 2. Copy the PAT โ”‚ + โ”‚ 2. Copy the PAT โ”‚ โ”‚ โ”‚ - โ”‚ โ–ถ Paste the PAT and press Enter: โ–ˆ โ”‚ + โ”‚ โ–ถ Paste the PAT and press Enter: โ–ˆ โ”‚ โ”‚ โ”‚ - โ”‚ โ† Back Esc โ”‚ + โ”‚ โ† Back Esc โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` From a3e227789c17783369e5d30c2b5ee75bc53e48a7 Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Wed, 31 Dec 2025 22:38:12 -0800 Subject: [PATCH 09/11] Refactor PAT login UI: standardize list item formatting for improved readability --- main.go | 12 ++++++------ main.md | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/main.go b/main.go index 0dbdc17..385cd7c 100644 --- a/main.go +++ b/main.go @@ -6755,24 +6755,24 @@ func (m patLoginModel) renderTokenInputView() string { b.WriteString(renderTitleBar("๐Ÿ”ง Setup / ๐Ÿ”‘ Login with PAT", m.currentUsername, m.currentOrg, innerWidth) + "\n") b.WriteString("\n") - b.WriteString(" 1. Opening browser to create new PAT at github.com\n") + b.WriteString("1. Opening browser to create new PAT at github.com\n") b.WriteString("\n") - b.WriteString(" 2. Copy the PAT\n") + b.WriteString("2. Copy the PAT\n") b.WriteString("\n") // Paste option if m.cursor == 0 { - b.WriteString(selectorStyle.Render("โ–ถ") + " Paste the PAT and press Enter: " + m.textInput.View() + "\n") + b.WriteString(selectorStyle.Render("โ–ถ") + " Paste the PAT and press Enter: " + m.textInput.View() + "\n") } else { - b.WriteString(" Paste the PAT and press Enter: " + m.textInput.View() + "\n") + b.WriteString(" Paste the PAT and press Enter: " + m.textInput.View() + "\n") } b.WriteString("\n") // Back option - styled like Setup menu (name always bold, description changes) if m.cursor == 1 { - b.WriteString(selectorStyle.Render("โ–ถ") + " โ† " + titleStyle.Render("Back") + " " + selectedStyle.Render("Esc") + "\n") + b.WriteString(selectorStyle.Render("โ–ถ") + " โ† " + titleStyle.Render("Back") + " " + selectedStyle.Render("Esc") + "\n") } else { - b.WriteString(" โ† " + titleStyle.Render("Back") + " " + dimStyle.Render("Esc") + "\n") + b.WriteString(" โ† " + titleStyle.Render("Back") + " " + dimStyle.Render("Esc") + "\n") } return b.String() diff --git a/main.md b/main.md index 7952139..9f3dfaa 100644 --- a/main.md +++ b/main.md @@ -297,13 +297,13 @@ Manual authentication using a Personal Access Token (PAT). Useful when OAuth flo โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿ”‘ Login with PAT 1.0.0 โ”‚ โ”‚ โ”‚ - โ”‚ 1. Opening browser to create new PAT at github.com โ”‚ + โ”‚ 1. Opening browser to create new PAT at github.com โ”‚ โ”‚ โ”‚ - โ”‚ 2. Copy the PAT โ”‚ + โ”‚ 2. Copy the PAT โ”‚ โ”‚ โ”‚ - โ”‚ โ–ถ Paste the PAT and press Enter: โ–ˆ โ”‚ + โ”‚ โ–ถ Paste the PAT and press Enter: โ–ˆ โ”‚ โ”‚ โ”‚ - โ”‚ โ† Back Esc โ”‚ + โ”‚ โ† Back Esc โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` @@ -313,13 +313,13 @@ Manual authentication using a Personal Access Token (PAT). Useful when OAuth flo โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ GitHub Brain / ๐Ÿ”ง Setup / ๐Ÿ”‘ Login with PAT ๐Ÿ‘ค @wham ยท ๐Ÿข my-org ยท 1.0.0 โ”‚ โ”‚ โ”‚ - โ”‚ 1. Opening browser to create new PAT at github.com โ”‚ + โ”‚ 1. Opening browser to create new PAT at github.com โ”‚ โ”‚ โ”‚ - โ”‚ 2. Copy the PAT โ”‚ + โ”‚ 2. Copy the PAT โ”‚ โ”‚ โ”‚ - โ”‚ โ–ถ Paste the PAT and press Enter: โ–ˆ โ”‚ + โ”‚ โ–ถ Paste the PAT and press Enter: โ–ˆ โ”‚ โ”‚ โ”‚ - โ”‚ โ† Back Esc โ”‚ + โ”‚ โ† Back Esc โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` From 439668c388889012fa27e72cf55b9566c115d52b Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Wed, 31 Dec 2025 22:47:29 -0800 Subject: [PATCH 10/11] Update PAT creation URL and enhance instructions for clarity in token setup --- main.go | 6 ++++-- main.md | 10 +++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 385cd7c..fc1e485 100644 --- a/main.go +++ b/main.go @@ -6612,7 +6612,7 @@ func (m patLoginModel) Init() tea.Cmd { func openPATCreationPage() tea.Cmd { return func() tea.Msg { // Open browser to pre-filled PAT creation page - patURL := "https://github.com/settings/personal-access-tokens/new?name=github-brain&description=http%3A%2F%2Fgithub.com%2Fwham%2Fgithub-brain&issues=read&pull_requests=read&discussions=read" + patURL := "https://github.com/settings/personal-access-tokens/new?name=github-brain&description=https%3A%2F%2Fgithub.com%2Fwham%2Fgithub-brain&issues=read&pull_requests=read&discussions=read" _ = browser.OpenURL(patURL) return nil } @@ -6757,7 +6757,9 @@ func (m patLoginModel) renderTokenInputView() string { b.WriteString("\n") b.WriteString("1. Opening browser to create new PAT at github.com\n") b.WriteString("\n") - b.WriteString("2. Copy the PAT\n") + b.WriteString("2. Set resource owner to the organization you want to use\n") + b.WriteString("\n") + b.WriteString("3. Copy the PAT\n") b.WriteString("\n") // Paste option diff --git a/main.md b/main.md index 9f3dfaa..a5001e0 100644 --- a/main.md +++ b/main.md @@ -288,7 +288,7 @@ Manual authentication using a Personal Access Token (PAT). Useful when OAuth flo 1. Open browser to pre-filled PAT creation page: ``` - https://github.com/settings/personal-access-tokens/new?name=github-brain&description=http%3A%2F%2Fgithub.com%2Fwham%2Fgithub-brain&issues=read&pull_requests=read&discussions=read + https://github.com/settings/personal-access-tokens/new?name=github-brain&description=https%3A%2F%2Fgithub.com%2Fwham%2Fgithub-brain&issues=read&pull_requests=read&discussions=read ``` 2. Display token input screen: @@ -299,7 +299,9 @@ Manual authentication using a Personal Access Token (PAT). Useful when OAuth flo โ”‚ โ”‚ โ”‚ 1. Opening browser to create new PAT at github.com โ”‚ โ”‚ โ”‚ - โ”‚ 2. Copy the PAT โ”‚ + โ”‚ 2. Set resource owner to the organization you want to use โ”‚ + โ”‚ โ”‚ + โ”‚ 3. Copy the PAT โ”‚ โ”‚ โ”‚ โ”‚ โ–ถ Paste the PAT and press Enter: โ–ˆ โ”‚ โ”‚ โ”‚ @@ -315,7 +317,9 @@ Manual authentication using a Personal Access Token (PAT). Useful when OAuth flo โ”‚ โ”‚ โ”‚ 1. Opening browser to create new PAT at github.com โ”‚ โ”‚ โ”‚ - โ”‚ 2. Copy the PAT โ”‚ + โ”‚ 2. Set resource owner to the organization you want to use โ”‚ + โ”‚ โ”‚ + โ”‚ 3. Copy the PAT โ”‚ โ”‚ โ”‚ โ”‚ โ–ถ Paste the PAT and press Enter: โ–ˆ โ”‚ โ”‚ โ”‚ From ded51338d2eb0cbc7b4ae2be70efcf30940453e7 Mon Sep 17 00:00:00 2001 From: Tomas Vesely <448809+wham@users.noreply.github.com> Date: Wed, 31 Dec 2025 23:37:58 -0800 Subject: [PATCH 11/11] Update PAT creation URL to include members permission and adjust status messages in the organization selection flow --- main.go | 12 +++++------- main.md | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index fc1e485..d20b715 100644 --- a/main.go +++ b/main.go @@ -6284,11 +6284,11 @@ func (m selectOrgModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.errorMsg = fmt.Sprintf("failed to save organization: %v", err) return m, nil } - m.status = "success" + // Go directly back to Setup menu + m.selectedOrg = msg.organization + m.status = "done" m.done = true - return m, tea.Tick(500*time.Millisecond, func(t time.Time) tea.Msg { - return tea.Quit() - }) + return m, tea.Quit case spinner.TickMsg: if m.status == "loading" { @@ -6325,8 +6325,6 @@ func (m selectOrgModel) View() string { content = m.renderListView() case "error": content = m.renderErrorView() - case "success": - content = m.renderSuccessView() } // Calculate box width @@ -6612,7 +6610,7 @@ func (m patLoginModel) Init() tea.Cmd { func openPATCreationPage() tea.Cmd { return func() tea.Msg { // Open browser to pre-filled PAT creation page - patURL := "https://github.com/settings/personal-access-tokens/new?name=github-brain&description=https%3A%2F%2Fgithub.com%2Fwham%2Fgithub-brain&issues=read&pull_requests=read&discussions=read" + patURL := "https://github.com/settings/personal-access-tokens/new?name=github-brain&description=https%3A%2F%2Fgithub.com%2Fwham%2Fgithub-brain&issues=read&pull_requests=read&discussions=read&members=read" _ = browser.OpenURL(patURL) return nil } diff --git a/main.md b/main.md index a5001e0..96d8597 100644 --- a/main.md +++ b/main.md @@ -288,7 +288,7 @@ Manual authentication using a Personal Access Token (PAT). Useful when OAuth flo 1. Open browser to pre-filled PAT creation page: ``` - https://github.com/settings/personal-access-tokens/new?name=github-brain&description=https%3A%2F%2Fgithub.com%2Fwham%2Fgithub-brain&issues=read&pull_requests=read&discussions=read + https://github.com/settings/personal-access-tokens/new?name=github-brain&description=https%3A%2F%2Fgithub.com%2Fwham%2Fgithub-brain&issues=read&pull_requests=read&discussions=read&members=read ``` 2. Display token input screen: