Skip to content
Merged
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
29 changes: 27 additions & 2 deletions Packages/CrowCore/Sources/CrowCore/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public final class AppState {
/// Fixed UUID for the allow list tab.
nonisolated public static let allowListSessionID = UUID(uuidString: "00000000-0000-0000-0000-000000000002")!

/// Fixed UUID for the review board tab.
nonisolated public static let reviewBoardSessionID = UUID(uuidString: "00000000-0000-0000-0000-000000000003")!

public var managerSession: Session? {
sessions.first { $0.id == Self.managerSessionID }
}
Expand Down Expand Up @@ -76,6 +79,20 @@ public final class AppState {
/// Must be cleaned up when a session is deleted (see `SessionService.deleteSession`).
public var prStatus: [UUID: PRStatus] = [:]

// MARK: - Review Requests

/// PRs where the current user has been requested as a reviewer.
public var reviewRequests: [ReviewRequest] = []
public var isLoadingReviews: Bool = false

/// IDs of review requests the user has already seen (for badge count).
public var seenReviewRequestIDs: Set<String> = []

/// Number of unseen review requests (for sidebar badge).
public var unseenReviewCount: Int {
reviewRequests.filter { !seenReviewRequestIDs.contains($0.id) }.count
}

/// Whether the VS Code `code` CLI is available on this system.
public var vsCodeAvailable: Bool = false

Expand Down Expand Up @@ -114,6 +131,9 @@ public final class AppState {
/// Called when user clicks "Work on" for an assigned issue.
public var onWorkOnIssue: ((String) -> Void)? // receives issue URL

/// Called when user clicks "Start Review" for a PR review request.
public var onStartReview: ((String) -> Void)? // receives PR URL

/// Called to launch Claude in a terminal that just became ready.
public var onLaunchClaude: ((UUID) -> Void)? // receives terminal ID

Expand Down Expand Up @@ -154,12 +174,13 @@ public final class AppState {

public var selectedSession: Session? {
guard selectedSessionID != Self.ticketBoardSessionID,
selectedSessionID != Self.allowListSessionID else { return nil }
selectedSessionID != Self.allowListSessionID,
selectedSessionID != Self.reviewBoardSessionID else { return nil }
return sessions.first { $0.id == selectedSessionID }
}

public var activeSessions: [Session] {
sessions.filter { $0.status == .active && $0.id != Self.managerSessionID }
sessions.filter { $0.status == .active && $0.id != Self.managerSessionID && $0.kind == .work }
}

public var inReviewSessions: [Session] {
Expand All @@ -170,6 +191,10 @@ public final class AppState {
sessions.filter { $0.status == .completed || $0.status == .archived }
}

public var reviewSessions: [Session] {
sessions.filter { $0.kind == .review && $0.status != .completed && $0.status != .archived }
}

public func worktrees(for sessionID: UUID) -> [SessionWorktree] {
worktrees[sessionID] ?? []
}
Expand Down
6 changes: 6 additions & 0 deletions Packages/CrowCore/Sources/CrowCore/Models/Enums.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import Foundation

/// Whether a session is a normal work session or a PR review session.
public enum SessionKind: String, Codable, Sendable {
case work // Normal development session (default)
case review // PR review session
}

/// Status of a development session.
public enum SessionStatus: String, Codable, Sendable {
case active
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,31 @@ import Foundation
public enum NotificationEvent: String, Codable, Sendable, CaseIterable, Identifiable {
case taskComplete
case agentWaiting
case reviewRequested

public var id: String { rawValue }

public var displayName: String {
switch self {
case .taskComplete: "Task Complete"
case .agentWaiting: "Agent Waiting"
case .reviewRequested: "Review Requested"
}
}

public var description: String {
switch self {
case .taskComplete: "Claude finished responding"
case .agentWaiting: "Claude needs your input or permission"
case .reviewRequested: "Someone requested your review on a PR"
}
}

public var defaultSound: String {
switch self {
case .taskComplete: "Glass"
case .agentWaiting: "Funk"
case .reviewRequested: "Glass"
}
}

Expand Down
45 changes: 45 additions & 0 deletions Packages/CrowCore/Sources/CrowCore/Models/ReviewRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation

/// A pull request where the current user has been requested as a reviewer.
public struct ReviewRequest: Identifiable, Codable, Sendable {
public let id: String // "github:org/repo#123"
public var prNumber: Int
public var title: String
public var url: String // full PR URL
public var repo: String // "org/repo"
public var author: String // PR author login
public var headBranch: String
public var baseBranch: String
public var isDraft: Bool
public var requestedAt: Date?
public var provider: Provider
public var reviewSessionID: UUID? // set if a review session already exists

public init(
id: String,
prNumber: Int,
title: String,
url: String,
repo: String,
author: String,
headBranch: String,
baseBranch: String,
isDraft: Bool = false,
requestedAt: Date? = nil,
provider: Provider = .github,
reviewSessionID: UUID? = nil
) {
self.id = id
self.prNumber = prNumber
self.title = title
self.url = url
self.repo = repo
self.author = author
self.headBranch = headBranch
self.baseBranch = baseBranch
self.isDraft = isDraft
self.requestedAt = requestedAt
self.provider = provider
self.reviewSessionID = reviewSessionID
}
}
18 changes: 18 additions & 0 deletions Packages/CrowCore/Sources/CrowCore/Models/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public struct Session: Identifiable, Codable, Sendable {
public let id: UUID
public var name: String
public var status: SessionStatus
public var kind: SessionKind
public var ticketURL: String?
public var ticketTitle: String?
public var ticketNumber: Int?
Expand All @@ -16,6 +17,7 @@ public struct Session: Identifiable, Codable, Sendable {
id: UUID = UUID(),
name: String,
status: SessionStatus = .active,
kind: SessionKind = .work,
ticketURL: String? = nil,
ticketTitle: String? = nil,
ticketNumber: Int? = nil,
Expand All @@ -26,11 +28,27 @@ public struct Session: Identifiable, Codable, Sendable {
self.id = id
self.name = name
self.status = status
self.kind = kind
self.ticketURL = ticketURL
self.ticketTitle = ticketTitle
self.ticketNumber = ticketNumber
self.provider = provider
self.createdAt = createdAt
self.updatedAt = updatedAt
}

// Backward-compatible decoding: default `kind` to `.work` when missing from older persisted data.
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)
status = try container.decode(SessionStatus.self, forKey: .status)
kind = try container.decodeIfPresent(SessionKind.self, forKey: .kind) ?? .work
ticketURL = try container.decodeIfPresent(String.self, forKey: .ticketURL)
ticketTitle = try container.decodeIfPresent(String.self, forKey: .ticketTitle)
ticketNumber = try container.decodeIfPresent(Int.self, forKey: .ticketNumber)
provider = try container.decodeIfPresent(Provider.self, forKey: .provider)
createdAt = try container.decode(Date.self, forKey: .createdAt)
updatedAt = try container.decode(Date.self, forKey: .updatedAt)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Testing
@testable import CrowCore

@Test func notificationEventAllCasesCount() {
#expect(NotificationEvent.allCases.count == 2)
#expect(NotificationEvent.allCases.count == 3)
}

@Test func notificationEventDefaultSoundsNonEmpty() {
Expand Down
2 changes: 2 additions & 0 deletions Packages/CrowUI/Sources/CrowUI/MainContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public struct MainContentView: View {
TicketBoardView(appState: appState)
} else if appState.selectedSessionID == AppState.allowListSessionID {
AllowListView(appState: appState)
} else if appState.selectedSessionID == AppState.reviewBoardSessionID {
ReviewBoardView(appState: appState)
} else if let session = appState.selectedSession {
SessionDetailView(session: session, appState: appState)
} else {
Expand Down
Loading
Loading