diff --git a/Packages/CrowCore/Sources/CrowCore/AppState.swift b/Packages/CrowCore/Sources/CrowCore/AppState.swift index 9009caa..4315946 100644 --- a/Packages/CrowCore/Sources/CrowCore/AppState.swift +++ b/Packages/CrowCore/Sources/CrowCore/AppState.swift @@ -103,12 +103,20 @@ public final class AppState { public var reviewRequests: [ReviewRequest] = [] public var isLoadingReviews: Bool = false + public var excludeReviewRepos: [String] = [] + + public var filteredReviewRequests: [ReviewRequest] { + guard !excludeReviewRepos.isEmpty else { return reviewRequests } + let excludeSet = Set(excludeReviewRepos.map { $0.lowercased() }) + return reviewRequests.filter { !excludeSet.contains($0.repo.lowercased()) } + } + /// IDs of review requests the user has already seen (for badge count). public var seenReviewRequestIDs: Set = [] /// Number of unseen review requests (for sidebar badge). public var unseenReviewCount: Int { - reviewRequests.filter { !seenReviewRequestIDs.contains($0.id) }.count + filteredReviewRequests.filter { !seenReviewRequestIDs.contains($0.id) }.count } /// Whether the VS Code `code` CLI is available on this system. diff --git a/Packages/CrowCore/Sources/CrowCore/Models/AppConfig.swift b/Packages/CrowCore/Sources/CrowCore/Models/AppConfig.swift index bc85d66..4d11316 100644 --- a/Packages/CrowCore/Sources/CrowCore/Models/AppConfig.swift +++ b/Packages/CrowCore/Sources/CrowCore/Models/AppConfig.swift @@ -114,6 +114,7 @@ public struct ConfigDefaults: Codable, Sendable, Equatable { public var cli: String public var branchPrefix: String public var excludeDirs: [String] + public var excludeReviewRepos: [String] /// Characters that are invalid in git ref names (see `git check-ref-format`). private static let invalidBranchChars = CharacterSet(charactersIn: " ~^:?*[\\") @@ -137,12 +138,27 @@ public struct ConfigDefaults: Codable, Sendable, Equatable { provider: String = "github", cli: String = "gh", branchPrefix: String = "feature/", - excludeDirs: [String] = ["node_modules", ".git", "vendor", "dist", "build", "target"] + excludeDirs: [String] = ["node_modules", ".git", "vendor", "dist", "build", "target"], + excludeReviewRepos: [String] = [] ) { self.provider = provider self.cli = cli self.branchPrefix = branchPrefix self.excludeDirs = excludeDirs + self.excludeReviewRepos = excludeReviewRepos + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + provider = try container.decodeIfPresent(String.self, forKey: .provider) ?? "github" + cli = try container.decodeIfPresent(String.self, forKey: .cli) ?? "gh" + branchPrefix = try container.decodeIfPresent(String.self, forKey: .branchPrefix) ?? "feature/" + excludeDirs = try container.decodeIfPresent([String].self, forKey: .excludeDirs) ?? ["node_modules", ".git", "vendor", "dist", "build", "target"] + excludeReviewRepos = try container.decodeIfPresent([String].self, forKey: .excludeReviewRepos) ?? [] + } + + private enum CodingKeys: String, CodingKey { + case provider, cli, branchPrefix, excludeDirs, excludeReviewRepos } } diff --git a/Packages/CrowCore/Tests/CrowCoreTests/AppConfigTests.swift b/Packages/CrowCore/Tests/CrowCoreTests/AppConfigTests.swift index 815cb57..5d6043a 100644 --- a/Packages/CrowCore/Tests/CrowCoreTests/AppConfigTests.swift +++ b/Packages/CrowCore/Tests/CrowCoreTests/AppConfigTests.swift @@ -8,7 +8,7 @@ import Testing WorkspaceInfo(name: "TestOrg", provider: "github", cli: "gh", alwaysInclude: ["repo1"]), WorkspaceInfo(name: "GitLabOrg", provider: "gitlab", cli: "glab", host: "gitlab.example.com"), ], - defaults: ConfigDefaults(provider: "gitlab", cli: "glab", branchPrefix: "fix/", excludeDirs: ["vendor"]), + defaults: ConfigDefaults(provider: "gitlab", cli: "glab", branchPrefix: "fix/", excludeDirs: ["vendor"], excludeReviewRepos: ["zarf-dev/zarf", "bmlt-enabled/yap"]), notifications: NotificationSettings(globalMute: true), sidebar: SidebarSettings(hideSessionDetails: true) ) @@ -25,6 +25,7 @@ import Testing #expect(decoded.defaults.provider == "gitlab") #expect(decoded.defaults.branchPrefix == "fix/") #expect(decoded.defaults.excludeDirs == ["vendor"]) + #expect(decoded.defaults.excludeReviewRepos == ["zarf-dev/zarf", "bmlt-enabled/yap"]) #expect(decoded.notifications.globalMute == true) #expect(decoded.sidebar.hideSessionDetails == true) } @@ -36,6 +37,7 @@ import Testing #expect(config.workspaces.isEmpty) #expect(config.defaults.provider == "github") #expect(config.defaults.branchPrefix == "feature/") + #expect(config.defaults.excludeReviewRepos.isEmpty) #expect(config.notifications.globalMute == false) #expect(config.sidebar.hideSessionDetails == false) #expect(config.remoteControlEnabled == false) @@ -105,6 +107,15 @@ import Testing #expect(a != c) } +@Test func configDefaultsDecodeWithoutExcludeReviewRepos() throws { + let json = """ + {"defaults": {"provider": "github", "cli": "gh", "branchPrefix": "feature/", "excludeDirs": ["node_modules"]}} + """.data(using: .utf8)! + let config = try JSONDecoder().decode(AppConfig.self, from: json) + #expect(config.defaults.excludeReviewRepos.isEmpty) + #expect(config.defaults.excludeDirs == ["node_modules"]) +} + // MARK: - WorkspaceInfo @Test func workspaceInfoDerivedCLI() { diff --git a/Packages/CrowUI/Sources/CrowUI/ReviewBoardView.swift b/Packages/CrowUI/Sources/CrowUI/ReviewBoardView.swift index a14723b..8a05d37 100644 --- a/Packages/CrowUI/Sources/CrowUI/ReviewBoardView.swift +++ b/Packages/CrowUI/Sources/CrowUI/ReviewBoardView.swift @@ -25,7 +25,7 @@ public struct ReviewBoardView: View { .background(.background) .onAppear { // Mark all current review requests as seen - for request in appState.reviewRequests { + for request in appState.filteredReviewRequests { appState.seenReviewRequestIDs.insert(request.id) } } @@ -44,7 +44,7 @@ public struct ReviewBoardView: View { Spacer() - Text("\(appState.reviewRequests.count) pending") + Text("\(appState.filteredReviewRequests.count) pending") .font(.caption) .foregroundStyle(CorveilTheme.textSecondary) } @@ -55,7 +55,7 @@ public struct ReviewBoardView: View { @ViewBuilder private var reviewList: some View { - if appState.reviewRequests.isEmpty { + if appState.filteredReviewRequests.isEmpty { VStack { Spacer().frame(height: 40) Image(systemName: "eye.circle") @@ -74,7 +74,7 @@ public struct ReviewBoardView: View { } .frame(maxWidth: .infinity) } else { - List(appState.reviewRequests) { request in + List(appState.filteredReviewRequests) { request in ReviewRow(request: request, appState: appState) } .listStyle(.inset) @@ -207,8 +207,8 @@ public struct ReviewTerminalsSidebarRow: View { ProgressView() .controlSize(.mini) } - if appState.reviewRequests.count > 0 { - Text("\(appState.reviewRequests.count)") + if appState.filteredReviewRequests.count > 0 { + Text("\(appState.filteredReviewRequests.count)") .font(.system(size: 10, weight: .semibold)) .monospacedDigit() .padding(.horizontal, 4) diff --git a/Packages/CrowUI/Sources/CrowUI/SettingsView.swift b/Packages/CrowUI/Sources/CrowUI/SettingsView.swift index 111f931..ac97e89 100644 --- a/Packages/CrowUI/Sources/CrowUI/SettingsView.swift +++ b/Packages/CrowUI/Sources/CrowUI/SettingsView.swift @@ -12,6 +12,7 @@ public struct SettingsView: View { @State var config: AppConfig @State private var isAddingWorkspace = false @State private var editingWorkspace: WorkspaceInfo? + @State private var excludeReviewReposText: String public var onSave: ((String, AppConfig) -> Void)? public var onRescaffold: ((String) -> Void)? @@ -22,6 +23,7 @@ public struct SettingsView: View { self.appState = appState self._devRoot = State(initialValue: devRoot) self._config = State(initialValue: config) + self._excludeReviewReposText = State(initialValue: config.defaults.excludeReviewRepos.joined(separator: ", ")) self.onSave = onSave self.onRescaffold = onRescaffold } @@ -156,6 +158,21 @@ public struct SettingsView: View { .foregroundStyle(.secondary) } + Section("Reviews") { + TextField("Excluded Repos", text: $excludeReviewReposText) + .textFieldStyle(.roundedBorder) + .onChange(of: excludeReviewReposText) { _, _ in + config.defaults.excludeReviewRepos = excludeReviewReposText + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + save() + } + Text("Comma-separated list of repos to hide from the review board (e.g., zarf-dev/zarf, bmlt-enabled/yap).") + .font(.caption) + .foregroundStyle(.secondary) + } + Section("Remote Control") { Toggle("Enable remote control for new sessions", isOn: $config.remoteControlEnabled) .onChange(of: config.remoteControlEnabled) { _, _ in save() } diff --git a/Sources/Crow/App/AppDelegate.swift b/Sources/Crow/App/AppDelegate.swift index bfb17c8..ec5e3e6 100644 --- a/Sources/Crow/App/AppDelegate.swift +++ b/Sources/Crow/App/AppDelegate.swift @@ -114,6 +114,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { // be rebuilt to include (or drop) `--rc` before its surface is pre-initialized. appState.remoteControlEnabled = config.remoteControlEnabled appState.managerAutoPermissionMode = config.managerAutoPermissionMode + appState.excludeReviewRepos = config.defaults.excludeReviewRepos // Create session service and hydrate state let service = SessionService(store: store, appState: appState, telemetryPort: config.telemetry.enabled ? config.telemetry.port : nil) @@ -448,6 +449,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { appState.hideSessionDetails = config.sidebar.hideSessionDetails appState.remoteControlEnabled = config.remoteControlEnabled appState.managerAutoPermissionMode = config.managerAutoPermissionMode + appState.excludeReviewRepos = config.defaults.excludeReviewRepos } // MARK: - Socket Server diff --git a/Sources/Crow/App/IssueTracker.swift b/Sources/Crow/App/IssueTracker.swift index 4d2e7d9..4d459c0 100644 --- a/Sources/Crow/App/IssueTracker.swift +++ b/Sources/Crow/App/IssueTracker.swift @@ -254,9 +254,14 @@ final class IssueTracker { reviews[i].reviewSessionID = session.id } } + let allCurrentIDs = Set(reviews.map(\.id)) + let excludeSet = Set(config.defaults.excludeReviewRepos.map { $0.lowercased() }) + if !excludeSet.isEmpty { + reviews = reviews.filter { !excludeSet.contains($0.repo.lowercased()) } + } let currentIDs = Set(reviews.map(\.id)) let newIDs = currentIDs.subtracting(previousReviewRequestIDs) - previousReviewRequestIDs = currentIDs + previousReviewRequestIDs = allCurrentIDs if !isFirstFetch && !newIDs.isEmpty { let newRequests = reviews.filter { newIDs.contains($0.id) } onNewReviewRequests?(newRequests) diff --git a/docs/configuration.md b/docs/configuration.md index cfa8be8..c84531d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -47,7 +47,8 @@ All persistent state lives under `~/Library/Application Support/crow/` (see `Pac "provider": "github", "cli": "gh", "branchPrefix": "feature/", - "excludeDirs": ["node_modules", ".git", "vendor", "dist", "build", "target"] + "excludeDirs": ["node_modules", ".git", "vendor", "dist", "build", "target"], + "excludeReviewRepos": ["zarf-dev/zarf", "bmlt-enabled/yap"] } } ``` @@ -57,6 +58,7 @@ All persistent state lives under `~/Library/Application Support/crow/` (see `Pac - **`host`** — set for self-hosted GitLab; exported as `GITLAB_HOST` when invoking `glab`. - **`branchPrefix`** — used by the `/crow-workspace` skill when creating new branches. - **`excludeDirs`** — ignored when scanning repos for git worktrees. +- **`excludeReviewRepos`** — repos to hide from the review board (e.g., `["zarf-dev/zarf"]`). Matching reviews are filtered out from the board, sidebar badge count, and notifications. ## Manager Terminal