Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion Packages/CrowCore/Sources/CrowCore/Models/AppConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public struct WorkspaceInfo: Identifiable, Codable, Sendable, Equatable {
public var cli: String // "gh" or "glab" — kept for config file compat
public var host: String? // GitLab host (e.g., "gitlab.example.com")
public var alwaysInclude: [String] // repos to always list in prompt table
public var autoReviewRepos: [String] // repos where review requests auto-create a review session

/// The CLI tool name derived from the current `provider` value.
/// Unlike `cli` (which may be stale from an old config file), this is always correct.
Expand All @@ -74,14 +75,31 @@ public struct WorkspaceInfo: Identifiable, Codable, Sendable, Equatable {
provider: String = "github",
cli: String = "gh",
host: String? = nil,
alwaysInclude: [String] = []
alwaysInclude: [String] = [],
autoReviewRepos: [String] = []
) {
self.id = id
self.name = name
self.provider = provider
self.cli = cli
self.host = host
self.alwaysInclude = alwaysInclude
self.autoReviewRepos = autoReviewRepos
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
provider = try container.decode(String.self, forKey: .provider)
cli = try container.decode(String.self, forKey: .cli)
host = try container.decodeIfPresent(String.self, forKey: .host)
alwaysInclude = try container.decodeIfPresent([String].self, forKey: .alwaysInclude) ?? []
autoReviewRepos = try container.decodeIfPresent([String].self, forKey: .autoReviewRepos) ?? []
}

private enum CodingKeys: String, CodingKey {
case id, name, provider, cli, host, alwaysInclude, autoReviewRepos
}

/// Characters that are unsafe in directory names (workspace names become directory names).
Expand Down
19 changes: 19 additions & 0 deletions Packages/CrowCore/Tests/CrowCoreTests/AppConfigTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,25 @@ import Testing
#expect(stale.derivedCLI == "glab")
}

@Test func workspaceAutoReviewReposRoundTrip() throws {
let config = AppConfig(workspaces: [
WorkspaceInfo(name: "Org", autoReviewRepos: ["org/repo1", "org/repo2"])
])
let data = try JSONEncoder().encode(config)
let decoded = try JSONDecoder().decode(AppConfig.self, from: data)
#expect(decoded.workspaces[0].autoReviewRepos == ["org/repo1", "org/repo2"])
}

@Test func workspaceAutoReviewReposDefaultsEmptyWhenKeyMissing() throws {
// Legacy configs without the key should default to empty (feature off).
let json = """
{"workspaces": [{"id": "00000000-0000-0000-0000-000000000001", "name": "Org", "provider": "github", "cli": "gh"}]}
""".data(using: .utf8)!
let config = try JSONDecoder().decode(AppConfig.self, from: json)
#expect(config.workspaces[0].autoReviewRepos.isEmpty)
#expect(config.workspaces[0].alwaysInclude.isEmpty)
}

@Test func workspaceNameValidation() {
// Valid name
#expect(WorkspaceInfo.validateName("MyOrg", existingNames: []) == nil)
Expand Down
18 changes: 16 additions & 2 deletions Packages/CrowUI/Sources/CrowUI/WorkspaceFormView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public struct WorkspaceFormView: View {
@State private var provider: String
@State private var host: String
@State private var alwaysIncludeText: String
@State private var autoReviewReposText: String

private let existingID: UUID?
private let existingNames: [String]
Expand All @@ -31,6 +32,7 @@ public struct WorkspaceFormView: View {
self._provider = State(initialValue: workspace?.provider ?? "github")
self._host = State(initialValue: workspace?.host ?? "")
self._alwaysIncludeText = State(initialValue: workspace?.alwaysInclude.joined(separator: ", ") ?? "")
self._autoReviewReposText = State(initialValue: workspace?.autoReviewRepos.joined(separator: ", ") ?? "")
self.existingNames = existingNames
self.onSave = onSave
}
Expand Down Expand Up @@ -70,6 +72,12 @@ public struct WorkspaceFormView: View {
Text("Comma-separated list of repos to always show in the workspace prompt.")
.font(.caption)
.foregroundStyle(.secondary)

TextField("Auto-Review Repos", text: $autoReviewReposText)
.textFieldStyle(.roundedBorder)
Text("Comma-separated repos (e.g. org/repo). New review requests from these repos will automatically create a review session.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.formStyle(.grouped)
Expand All @@ -83,13 +91,19 @@ public struct WorkspaceFormView: View {
.map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.isEmpty }

let autoReviewRepos = autoReviewReposText
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.isEmpty }

let ws = WorkspaceInfo(
id: existingID ?? UUID(),
name: trimmedName,
provider: provider,
cli: provider == "github" ? "gh" : "glab",
host: provider == "gitlab" && !host.isEmpty ? host : nil,
alwaysInclude: alwaysInclude
alwaysInclude: alwaysInclude,
autoReviewRepos: autoReviewRepos
)
onSave(ws)
dismiss()
Expand All @@ -99,6 +113,6 @@ public struct WorkspaceFormView: View {
}
.padding()
}
.frame(width: 400, height: 300)
.frame(width: 440, height: 400)
}
}
13 changes: 12 additions & 1 deletion Sources/Crow/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
// Start issue tracker
let tracker = IssueTracker(appState: appState)
tracker.onNewReviewRequests = { [weak self] newRequests in
guard let self else { return }
for request in newRequests {
self?.notificationManager?.notifyReviewRequest(request)
self.notificationManager?.notifyReviewRequest(request)

// Auto-start a review session if any configured workspace opts this repo in,
// and we don't already have a review session for this PR.
guard request.reviewSessionID == nil else { continue }
let enabledRepos = (self.appConfig?.workspaces ?? [])
.flatMap(\.autoReviewRepos)
.map { $0.lowercased() }
if enabledRepos.contains(request.repo.lowercased()) {
Task { await self.sessionService?.createReviewSession(prURL: request.url) }
}
}
}
tracker.start()
Expand Down
Loading