Add session filter to Activity view

Dropdown in the filter bar lets users scope activity to a single
session or view all. Sessions are labeled with their first user
message preview. Combines with the existing tool-kind filter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-03-31 03:03:28 -04:00
parent 0a73aab825
commit 0d38856b3e
2 changed files with 42 additions and 13 deletions
@@ -6,9 +6,20 @@ final class ActivityViewModel {
var toolMessages: [HermesMessage] = [] var toolMessages: [HermesMessage] = []
var filterKind: ToolKind? var filterKind: ToolKind?
var filterSessionId: String?
var selectedEntry: ActivityEntry? var selectedEntry: ActivityEntry?
var sessionPreviews: [String: String] = [:]
var isLoading = true var isLoading = true
var availableSessions: [(id: String, label: String)] {
var seen = Set<String>()
return toolMessages.compactMap { message in
guard seen.insert(message.sessionId).inserted else { return nil }
let label = sessionPreviews[message.sessionId] ?? message.sessionId
return (id: message.sessionId, label: label)
}
}
var filteredActivity: [ActivityEntry] { var filteredActivity: [ActivityEntry] {
let entries = toolMessages.flatMap { message in let entries = toolMessages.flatMap { message in
message.toolCalls.map { call in message.toolCalls.map { call in
@@ -24,10 +35,11 @@ final class ActivityViewModel {
) )
} }
} }
if let filterKind { return entries.filter { entry in
return entries.filter { $0.kind == filterKind } let kindOk = filterKind == nil || entry.kind == filterKind
let sessionOk = filterSessionId == nil || entry.sessionId == filterSessionId
return kindOk && sessionOk
} }
return entries
} }
func load() async { func load() async {
@@ -38,6 +50,7 @@ final class ActivityViewModel {
return return
} }
toolMessages = await dataService.fetchRecentToolCalls(limit: 200) toolMessages = await dataService.fetchRecentToolCalls(limit: 200)
sessionPreviews = await dataService.fetchSessionPreviews(limit: 200)
isLoading = false isLoading = false
} }
@@ -21,20 +21,36 @@ struct ActivityView: View {
} }
private var filterBar: some View { private var filterBar: some View {
ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) {
HStack(spacing: 8) { ScrollView(.horizontal, showsIndicators: false) {
FilterChip(label: "All", isSelected: viewModel.filterKind == nil) { HStack(spacing: 8) {
viewModel.filterKind = nil FilterChip(label: "All", isSelected: viewModel.filterKind == nil) {
} viewModel.filterKind = nil
ForEach(ToolKind.allCases, id: \.rawValue) { kind in }
FilterChip(label: kind.rawValue.capitalized, isSelected: viewModel.filterKind == kind) { ForEach(ToolKind.allCases, id: \.rawValue) { kind in
viewModel.filterKind = kind FilterChip(label: kind.rawValue.capitalized, isSelected: viewModel.filterKind == kind) {
viewModel.filterKind = kind
}
} }
} }
} }
.padding(.horizontal) Divider()
.padding(.vertical, 8) .frame(height: 16)
Picker(selection: $viewModel.filterSessionId) {
Text("All Sessions").tag(String?.none)
Divider()
ForEach(viewModel.availableSessions, id: \.id) { session in
Text(session.label)
.lineLimit(1)
.tag(String?.some(session.id))
}
} label: {
EmptyView()
}
.frame(maxWidth: 250)
} }
.padding(.horizontal)
.padding(.vertical, 8)
} }
private var activityList: some View { private var activityList: some View {