mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af8e120c9f | |||
| 0d38856b3e |
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
## Features
|
## 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)
|
- **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
|
- **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)
|
- **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 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,6 +21,7 @@ struct ActivityView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var filterBar: some View {
|
private var filterBar: some View {
|
||||||
|
HStack(spacing: 12) {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
FilterChip(label: "All", isSelected: viewModel.filterKind == nil) {
|
FilterChip(label: "All", isSelected: viewModel.filterKind == nil) {
|
||||||
@@ -32,10 +33,25 @@ struct ActivityView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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(.horizontal)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private var activityList: some View {
|
private var activityList: some View {
|
||||||
List(selection: Binding(
|
List(selection: Binding(
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ struct DashboardView: View {
|
|||||||
StatCard(label: "Messages", value: "\(viewModel.stats.totalMessages)")
|
StatCard(label: "Messages", value: "\(viewModel.stats.totalMessages)")
|
||||||
StatCard(label: "Tool Calls", value: "\(viewModel.stats.totalToolCalls)")
|
StatCard(label: "Tool Calls", value: "\(viewModel.stats.totalToolCalls)")
|
||||||
StatCard(label: "Tokens", value: formatTokens(viewModel.stats.totalInputTokens + viewModel.stats.totalOutputTokens))
|
StatCard(label: "Tokens", value: formatTokens(viewModel.stats.totalInputTokens + viewModel.stats.totalOutputTokens))
|
||||||
StatCard(label: "Est. Cost", value: String(format: "$%.2f", viewModel.stats.totalCostUSD))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user