Compare commits

...

2 Commits

Author SHA1 Message Date
Alan Wizemann af8e120c9f Remove cost stat card from dashboard
Hermes cost tracking returns $0.00 for models not in its static
pricing table (including claude-haiku-4-5). Token counts remain
displayed since those are always accurate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 03:12:04 -04:00
Alan Wizemann 0d38856b3e 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>
2026-03-31 03:03:28 -04:00
4 changed files with 43 additions and 15 deletions
+1 -1
View File
@@ -17,7 +17,7 @@
## Features
- **Dashboard** — System health, token usage, cost tracking, recent sessions at a glance
- **Dashboard** — System health, token usage, recent sessions at a glance
- **Sessions Browser** — Full conversation history with message rendering, tool call inspection, and full-text search (FTS5)
- **Activity Feed** — Recent tool execution log with filtering by kind (read/edit/execute/fetch/browser) and detail inspector
- **Live Chat** — Embedded terminal running `hermes chat` with full ANSI color and Rich formatting via [SwiftTerm](https://github.com/migueldeicaza/SwiftTerm)
@@ -6,9 +6,20 @@ final class ActivityViewModel {
var toolMessages: [HermesMessage] = []
var filterKind: ToolKind?
var filterSessionId: String?
var selectedEntry: ActivityEntry?
var sessionPreviews: [String: String] = [:]
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] {
let entries = toolMessages.flatMap { message in
message.toolCalls.map { call in
@@ -24,10 +35,11 @@ final class ActivityViewModel {
)
}
}
if let filterKind {
return entries.filter { $0.kind == filterKind }
return entries.filter { entry in
let kindOk = filterKind == nil || entry.kind == filterKind
let sessionOk = filterSessionId == nil || entry.sessionId == filterSessionId
return kindOk && sessionOk
}
return entries
}
func load() async {
@@ -38,6 +50,7 @@ final class ActivityViewModel {
return
}
toolMessages = await dataService.fetchRecentToolCalls(limit: 200)
sessionPreviews = await dataService.fetchSessionPreviews(limit: 200)
isLoading = false
}
@@ -21,20 +21,36 @@ struct ActivityView: View {
}
private var filterBar: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
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) {
viewModel.filterKind = kind
HStack(spacing: 12) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
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) {
viewModel.filterKind = kind
}
}
}
}
.padding(.horizontal)
.padding(.vertical, 8)
Divider()
.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 {
@@ -56,7 +56,6 @@ struct DashboardView: View {
StatCard(label: "Messages", value: "\(viewModel.stats.totalMessages)")
StatCard(label: "Tool Calls", value: "\(viewModel.stats.totalToolCalls)")
StatCard(label: "Tokens", value: formatTokens(viewModel.stats.totalInputTokens + viewModel.stats.totalOutputTokens))
StatCard(label: "Est. Cost", value: String(format: "$%.2f", viewModel.stats.totalCostUSD))
}
}
}