diff --git a/cmd/nightshift/commands/run.go b/cmd/nightshift/commands/run.go index 1a4ea65..9681002 100644 --- a/cmd/nightshift/commands/run.go +++ b/cmd/nightshift/commands/run.go @@ -212,11 +212,6 @@ func runRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("resolve projects: %w", err) } - // Limit projects when --project was not explicitly set - if projectPath == "" && maxProjects > 0 && len(projects) > maxProjects { - projects = projects[:maxProjects] - } - if len(projects) == 0 { fmt.Println("no projects configured") return nil @@ -251,6 +246,7 @@ func runRun(cmd *cobra.Command, args []string) error { st: st, projects: projects, taskFilter: taskFilter, + maxProjects: maxProjects, maxTasks: maxTasks, randomTask: randomTask, ignoreBudget: ignoreBudget, @@ -273,6 +269,7 @@ type executeRunParams struct { st *state.State projects []string taskFilter string + maxProjects int maxTasks int randomTask bool ignoreBudget bool @@ -438,7 +435,13 @@ func buildPreflight(p executeRunParams) (*preflightPlan, error) { branch: p.branch, } + eligibleCount := 0 for _, projectPath := range p.projects { + // Apply --max-projects limit (counts only eligible, non-skipped projects) + if p.maxProjects > 0 && eligibleCount >= p.maxProjects { + break + } + // Skip if already processed today (unless task filter specified) if p.taskFilter == "" && p.st.WasProcessedToday(projectPath) { p.log.Infof("skip %s (processed today)", projectPath) @@ -513,6 +516,9 @@ func buildPreflight(p executeRunParams) (*preflightPlan, error) { } plan.projects = append(plan.projects, pp) + if pp.skipReason == "" { + eligibleCount++ + } } return plan, nil diff --git a/cmd/nightshift/commands/run_test.go b/cmd/nightshift/commands/run_test.go index 9feae31..256f698 100644 --- a/cmd/nightshift/commands/run_test.go +++ b/cmd/nightshift/commands/run_test.go @@ -242,56 +242,68 @@ func newTestRunConfig() *config.Config { } } -func TestMaxProjects_DefaultLimitsToOne(t *testing.T) { - // Simulate 3 projects, no --project set, maxProjects=1 (default) - projects := []string{"/proj/a", "/proj/b", "/proj/c"} - projectPath := "" // not explicitly set - maxProjects := 1 +// TestMaxProjects_SkipsProcessedBeforeCounting verifies that --max-projects counts +// only eligible (non-processed-today) projects. With project[0] already processed +// and maxProjects=1, project[1] should be the one that gets processed. +func TestMaxProjects_SkipsProcessedBeforeCounting(t *testing.T) { + p0 := t.TempDir() + p1 := t.TempDir() + p2 := t.TempDir() + params := newPreflightParams(t, []string{p0, p1, p2}) + params.maxProjects = 1 + + // Mark p0 as already processed today + params.st.RecordProjectRun(p0) - if projectPath == "" && maxProjects > 0 && len(projects) > maxProjects { - projects = projects[:maxProjects] - } - - if len(projects) != 1 { - t.Fatalf("len(projects) = %d, want 1", len(projects)) - } - if projects[0] != "/proj/a" { - t.Fatalf("projects[0] = %q, want /proj/a", projects[0]) + plan, err := buildPreflight(params) + if err != nil { + t.Fatalf("buildPreflight: %v", err) } -} - -func TestMaxProjects_OverrideToN(t *testing.T) { - projects := []string{"/proj/a", "/proj/b", "/proj/c"} - projectPath := "" - maxProjects := 2 - if projectPath == "" && maxProjects > 0 && len(projects) > maxProjects { - projects = projects[:maxProjects] + // p0 should be skipped (processed today), p1 should be eligible and counted, + // p2 should not be reached (limit hit). + eligibleCount := 0 + var eligiblePath string + for _, pp := range plan.projects { + if pp.skipReason == "" { + eligibleCount++ + eligiblePath = pp.path + } } - - if len(projects) != 2 { - t.Fatalf("len(projects) = %d, want 2", len(projects)) + if eligibleCount != 1 { + t.Fatalf("eligible projects = %d, want 1", eligibleCount) } - if projects[1] != "/proj/b" { - t.Fatalf("projects[1] = %q, want /proj/b", projects[1]) + if eligiblePath != p1 { + t.Fatalf("eligible project = %q, want p1 (%q)", eligiblePath, p1) } } -func TestMaxProjects_IgnoredWhenProjectSet(t *testing.T) { - projects := []string{"/proj/explicit"} - projectPath := "/proj/explicit" // explicitly set - maxProjects := 1 +// TestMaxProjects_LimitsEligibleCount verifies that with no projects processed, +// --max-projects 2 processes exactly 2 projects. +func TestMaxProjects_LimitsEligibleCount(t *testing.T) { + p0 := t.TempDir() + p1 := t.TempDir() + p2 := t.TempDir() + params := newPreflightParams(t, []string{p0, p1, p2}) + params.maxProjects = 2 - // The guard: projectPath == "" is false, so no truncation - if projectPath == "" && maxProjects > 0 && len(projects) > maxProjects { - projects = projects[:maxProjects] + plan, err := buildPreflight(params) + if err != nil { + t.Fatalf("buildPreflight: %v", err) } - if len(projects) != 1 { - t.Fatalf("len(projects) = %d, want 1", len(projects)) + eligibleCount := 0 + for _, pp := range plan.projects { + if pp.skipReason == "" { + eligibleCount++ + } + } + if eligibleCount != 2 { + t.Fatalf("eligible projects = %d, want 2", eligibleCount) } - if projects[0] != "/proj/explicit" { - t.Fatalf("projects[0] = %q, want /proj/explicit", projects[0]) + // Total projects in plan should be 2 (p2 never added) + if len(plan.projects) != 2 { + t.Fatalf("plan.projects len = %d, want 2", len(plan.projects)) } } @@ -1257,7 +1269,7 @@ func TestDisplayPreflight_NoWarningsWhenBudgetRespected(t *testing.T) { func TestScheduleMaxProjectsFromConfig(t *testing.T) { // Simulate what runRun does: after loading config, apply schedule.MaxProjects // when the flag was not explicitly changed by the user. - maxProjects := 1 // CLI default + maxProjects := 1 // CLI default maxProjectsChanged := false // --max-projects was NOT passed cfg := &config.Config{ diff --git a/internal/agents/copilot.go b/internal/agents/copilot.go index e5451ae..fd94226 100644 --- a/internal/agents/copilot.go +++ b/internal/agents/copilot.go @@ -25,10 +25,10 @@ import ( // - Standalone: npm install -g @github/copilot or curl script // - Usage: copilot -p "" --no-ask-user --silent type CopilotAgent struct { - binaryPath string // Path to binary: "gh" or "copilot" (default: "gh") - dangerouslySkipPerms bool // Pass --allow-all-tools --allow-all-urls - timeout time.Duration // Default timeout - runner CommandRunner // Command executor (for testing) + binaryPath string // Path to binary: "gh" or "copilot" (default: "gh") + dangerouslySkipPerms bool // Pass --allow-all-tools --allow-all-urls + timeout time.Duration // Default timeout + runner CommandRunner // Command executor (for testing) } // CopilotOption configures a CopilotAgent. diff --git a/website/src/css/custom.css b/website/src/css/custom.css index d5e45a9..4a61ff5 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -894,3 +894,8 @@ html[data-theme='dark'] .footer { animation: none; } } + +/* Fix mobile navbar sidebar: prevent secondary panel from translating off-screen */ +.navbar-sidebar__items--show-secondary { + transform: none !important; +}