From 5bfc18c7c3c683cf4460242f52b147b3423e5900 Mon Sep 17 00:00:00 2001 From: Jonatan Dahl Date: Wed, 28 Jan 2026 14:18:09 -0500 Subject: [PATCH 1/3] feat: display status messages in gray/dim color Add dim color styling to in-progress spinner messages to visually distinguish them from success/error messages. Co-Authored-By: Claude Opus 4.5 --- internal/spinner/spinner.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/spinner/spinner.go b/internal/spinner/spinner.go index e1a7f51..ac2bf42 100644 --- a/internal/spinner/spinner.go +++ b/internal/spinner/spinner.go @@ -110,7 +110,7 @@ func (s *Spinner) run() { case <-ticker.C: s.mu.Lock() frame := s.frames[frameIdx%len(s.frames)] - fmt.Fprintf(s.writer, "\r%s %s", frame, s.message) + fmt.Fprintf(s.writer, "\r%s %s", frame, dim.Sprint(s.message)) s.mu.Unlock() frameIdx++ } @@ -133,13 +133,14 @@ func Wrap(message string, fn func() error) error { var ( green = color.New(color.FgGreen) red = color.New(color.FgRed) + dim = color.New(color.Faint) ) // WrapWithSuccess runs a function with a spinner and shows success/error message func WrapWithSuccess(message, successMessage string, fn func() error) error { if !Enabled { // When disabled (verbose mode), print message and run - fmt.Println(message) + fmt.Println(dim.Sprint(message)) err := fn() if err != nil { fmt.Printf("%s Error: %v\n", red.Sprint("✗"), err) @@ -160,7 +161,7 @@ func WrapWithSuccess(message, successMessage string, fn func() error) error { func WrapWithSuccessIndented(indent, message, successMessage string, fn func() error) error { if !Enabled { // When disabled (verbose mode), print message and run - fmt.Println(indent + message) + fmt.Println(indent + dim.Sprint(message)) err := fn() if err != nil { fmt.Printf("%s%s Error: %v\n", indent, red.Sprint("✗"), err) From 9cb2ab2af0123783aee765e13643b9f57ee5670c Mon Sep 17 00:00:00 2001 From: Jonatan Dahl Date: Wed, 28 Jan 2026 14:18:57 -0500 Subject: [PATCH 2/3] fix: don't suggest stacking base branch onto itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When running `stack status` on the base branch (e.g., main), the command would incorrectly offer to add it to a stack with itself as parent. Accepting this created a circular dependency (main → main). Now detects when the current branch is the base branch and shows a helpful message instead of the problematic prompt. Co-Authored-By: Claude Opus 4.5 --- cmd/status.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmd/status.go b/cmd/status.go index e81b31e..fe84b44 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -143,6 +143,14 @@ func runStatus(gitClient git.GitClient, githubClient github.GitHubClient) error // Check this BEFORE waiting for PR fetch to avoid long delays if tree == nil { baseBranch := stack.GetBaseBranch(gitClient) + + // Don't offer to add the base branch to a stack - it can't have a parent + if currentBranch == baseBranch { + fmt.Printf("Branch '%s' is the base branch and cannot be part of a stack.\n", ui.Branch(currentBranch)) + fmt.Printf("\nUse '%s' to create a new stack branch.\n", ui.Command("stack new ")) + return nil + } + fmt.Printf("Current branch '%s' is not part of a stack.\n\n", ui.Branch(currentBranch)) fmt.Printf("Add to stack with '%s' as parent? [Y/n] ", ui.Branch(baseBranch)) From aaa4f1a2b6e4e1fb36fd2db9319896763266257a Mon Sep 17 00:00:00 2001 From: Jonatan Dahl Date: Wed, 28 Jan 2026 14:59:13 -0500 Subject: [PATCH 3/3] fix: keep spinner running during PR fetch in status Move wg.Wait() and individual PR fetching inside the spinner block so the "Loading stack..." spinner displays continuously until the tree is ready to print, eliminating the ~3 second feedback gap. Co-Authored-By: Claude Opus 4.5 --- cmd/status.go | 68 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/cmd/status.go b/cmd/status.go index fe84b44..b27fd61 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -93,7 +93,7 @@ func runStatus(gitClient git.GitClient, githubClient github.GitHubClient) error prCache = make(map[string]*github.PRInfo) } - // Build the stack tree (runs in parallel with PR fetch) + // Build the stack tree AND wait for PR fetch (runs in parallel) if err := spinner.WrapWithAutoDelay("Loading stack...", 300*time.Millisecond, func() error { // Get current branch var err error @@ -125,6 +125,45 @@ func runStatus(gitClient git.GitClient, githubClient github.GitHubClient) error // Get ALL branch names in the tree (including intermediate branches without stackparent) allTreeBranches = getAllBranchNamesFromTree(tree) + + // Wait for PR fetch to complete (if running) + if !noPR { + wg.Wait() + + // GetAllPRs only fetches open PRs (to avoid 502 timeouts on large repos). + // For branches in our stack that aren't in the cache, check individually + // to detect merged PRs that need special handling. + // OPTIMIZATION: Only check branches in the current tree, not all stack branches. + branchSet := make(map[string]bool) + for _, name := range allTreeBranches { + branchSet[name] = true + } + baseBranch := stack.GetBaseBranch(gitClient) + + for _, branch := range stackBranches { + // Skip branches not in the current tree + if !branchSet[branch.Name] { + continue + } + // Skip if already in cache (has open PR) + if _, exists := prCache[branch.Name]; exists { + continue + } + // Fetch PR info for this branch (might be merged or non-existent) + if pr, err := githubClient.GetPRForBranch(branch.Name); err == nil && pr != nil { + prCache[branch.Name] = pr + } + // Also check parent if not in cache and not base branch + if branch.Parent != baseBranch { + if _, exists := prCache[branch.Parent]; !exists { + if pr, err := githubClient.GetPRForBranch(branch.Parent); err == nil && pr != nil { + prCache[branch.Parent] = pr + } + } + } + } + } + return nil }); err != nil { return err @@ -175,33 +214,6 @@ func runStatus(gitClient git.GitClient, githubClient github.GitHubClient) error return nil } - // Wait for PR fetch to complete (if running) - if !noPR { - wg.Wait() - - // GetAllPRs only fetches open PRs (to avoid 502 timeouts on large repos). - // For branches in our stack that aren't in the cache, check individually - // to detect merged PRs that need special handling. - for _, branch := range stackBranches { - // Skip if already in cache (has open PR) - if _, exists := prCache[branch.Name]; exists { - continue - } - // Fetch PR info for this branch (might be merged or non-existent) - if pr, err := githubClient.GetPRForBranch(branch.Name); err == nil && pr != nil { - prCache[branch.Name] = pr - } - // Also check parent if not in cache and not base branch - if branch.Parent != stack.GetBaseBranch(gitClient) { - if _, exists := prCache[branch.Parent]; !exists { - if pr, err := githubClient.GetPRForBranch(branch.Parent); err == nil && pr != nil { - prCache[branch.Parent] = pr - } - } - } - } - } - // Print the tree fmt.Println() printTree(gitClient, tree, "", true, currentBranch, prCache)