Skip to content
Closed
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
58 changes: 54 additions & 4 deletions Packages/CrowCore/Sources/CrowCore/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,20 +189,68 @@ public final class AppState {
return sessions.first { $0.id == selectedSessionID }
}

// MARK: - Attention Section

/// Sessions that need user attention (permission prompt, question, or task done).
/// Sorted by longest-waiting first.
public var attentionSessions: [Session] {
sessions
.filter { session in
guard session.id != Self.managerSessionID else { return false }
guard session.status != .completed && session.status != .archived else { return false }
let state = hookState(for: session.id)
return state.pendingNotification != nil || state.claudeState == .done
}
.sorted { a, b in
attentionTimestamp(for: a.id) < attentionTimestamp(for: b.id)
}
}

/// IDs of sessions currently in the Attention section, for filtering from normal groups.
public var attentionSessionIDs: Set<UUID> {
Set(attentionSessions.map(\.id))
}

/// Timestamp when a session entered its attention-worthy state.
private func attentionTimestamp(for sessionID: UUID) -> Date {
let state = hookState(for: sessionID)
if let notification = state.pendingNotification {
return notification.timestamp
}
if let lastEvent = state.hookEvents.last {
return lastEvent.timestamp
}
return .distantFuture
}

// MARK: - Session Groups

public var activeSessions: [Session] {
sessions.filter { $0.status == .active && $0.id != Self.managerSessionID && $0.kind == .work }
let attentionIDs = attentionSessionIDs
return sessions.filter {
$0.status == .active && $0.id != Self.managerSessionID && $0.kind == .work
&& !attentionIDs.contains($0.id)
}
}

public var inReviewSessions: [Session] {
sessions.filter { $0.status == .inReview && $0.id != Self.managerSessionID }
let attentionIDs = attentionSessionIDs
return sessions.filter {
$0.status == .inReview && $0.id != Self.managerSessionID
&& !attentionIDs.contains($0.id)
}
}

public var completedSessions: [Session] {
sessions.filter { $0.status == .completed || $0.status == .archived }
}

public var reviewSessions: [Session] {
sessions.filter { $0.kind == .review && $0.status != .completed && $0.status != .archived }
let attentionIDs = attentionSessionIDs
return sessions.filter {
$0.kind == .review && $0.status != .completed && $0.status != .archived
&& !attentionIDs.contains($0.id)
}
}

public func worktrees(for sessionID: UUID) -> [SessionWorktree] {
Expand Down Expand Up @@ -275,8 +323,10 @@ public final class AppState {
}

/// Find the active session linked to a given issue (by matching ticket URL).
/// Checks all sessions (not just the filtered `activeSessions` group) so that
/// sessions temporarily in the Attention section are still found.
public func activeSession(for issue: AssignedIssue) -> Session? {
activeSessions.first { $0.ticketURL == issue.url }
sessions.first { $0.status == .active && $0.ticketURL == issue.url }
}

/// Maps `.unknown` project status to `.backlog` for display purposes.
Expand Down
44 changes: 35 additions & 9 deletions Packages/CrowUI/Sources/CrowUI/SessionListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ public struct SessionListView: View {
.listRowBackground(Color.clear)
}

// Attention section (sessions needing user input)
if !appState.attentionSessions.isEmpty {
SectionDivider(title: "Attention", count: appState.attentionSessions.count)
ForEach(filteredSessions(appState.attentionSessions)) { session in
SessionRow(session: session, appState: appState)
.tag(session.id)
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
.contextMenu {
sessionContextMenu(session)
}
}
}

// Active sessions
if !appState.activeSessions.isEmpty {
SectionDivider(title: "Active")
Expand Down Expand Up @@ -170,16 +184,27 @@ struct SidebarBrandmark: View {
/// Uppercase section label used to group sessions in the sidebar.
struct SectionDivider: View {
let title: String
var count: Int? = nil

var body: some View {
Text(title)
.font(.system(size: 10, weight: .bold))
.tracking(1.5)
.textCase(.uppercase)
.foregroundStyle(CorveilTheme.goldDark)
.padding(.top, 10)
.padding(.bottom, 2)
.listRowSeparator(.hidden)
HStack(spacing: 6) {
Text(title)
.font(.system(size: 10, weight: .bold))
.tracking(1.5)
.textCase(.uppercase)
.foregroundStyle(CorveilTheme.goldDark)
if let count, count > 0 {
Text("\(count)")
.font(.system(size: 9, weight: .bold))
.foregroundStyle(.white)
.padding(.horizontal, 5)
.padding(.vertical, 1)
.background(Capsule().fill(.orange))
}
}
.padding(.top, 10)
.padding(.bottom, 2)
.listRowSeparator(.hidden)
}
}

Expand Down Expand Up @@ -448,7 +473,8 @@ struct SessionRow: View {
}

private var needsAttention: Bool {
appState.hookState(for: session.id).pendingNotification != nil
let state = appState.hookState(for: session.id)
return state.pendingNotification != nil || state.claudeState == .done
}

private var rowBackgroundColor: Color {
Expand Down
Loading