mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
Show session previews instead of raw IDs everywhere
Fetches the first user message per session from the database and uses it as the display label. Falls back to session title, then ID. Applied consistently across: - Chat session resume menu (shows what each session was about) - Sessions browser list and detail header - Dashboard recent sessions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -114,6 +114,34 @@ actor HermesDataService {
|
||||
return messages
|
||||
}
|
||||
|
||||
func fetchSessionPreviews(limit: Int = 10) -> [String: String] {
|
||||
guard let db else { return [:] }
|
||||
let sql = """
|
||||
SELECT m.session_id, substr(m.content, 1, 100)
|
||||
FROM messages m
|
||||
INNER JOIN (
|
||||
SELECT session_id, MIN(id) as min_id
|
||||
FROM messages
|
||||
WHERE role = 'user' AND content <> ''
|
||||
GROUP BY session_id
|
||||
) first ON m.id = first.min_id
|
||||
ORDER BY m.timestamp DESC
|
||||
LIMIT ?
|
||||
"""
|
||||
var stmt: OpaquePointer?
|
||||
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else { return [:] }
|
||||
defer { sqlite3_finalize(stmt) }
|
||||
sqlite3_bind_int(stmt, 1, Int32(limit))
|
||||
|
||||
var previews: [String: String] = [:]
|
||||
while sqlite3_step(stmt) == SQLITE_ROW {
|
||||
let sessionId = columnText(stmt!, 0)
|
||||
let preview = columnText(stmt!, 1)
|
||||
previews[sessionId] = preview
|
||||
}
|
||||
return previews
|
||||
}
|
||||
|
||||
struct SessionStats: Sendable {
|
||||
let totalSessions: Int
|
||||
let totalMessages: Int
|
||||
|
||||
@@ -7,6 +7,7 @@ final class ChatViewModel {
|
||||
private let dataService = HermesDataService()
|
||||
|
||||
var recentSessions: [HermesSession] = []
|
||||
var sessionPreviews: [String: String] = [:]
|
||||
var terminalView: LocalProcessTerminalView?
|
||||
var hasActiveProcess = false
|
||||
private var coordinator: Coordinator?
|
||||
@@ -31,9 +32,16 @@ final class ChatViewModel {
|
||||
let opened = await dataService.open()
|
||||
guard opened else { return }
|
||||
recentSessions = await dataService.fetchSessions(limit: 10)
|
||||
sessionPreviews = await dataService.fetchSessionPreviews(limit: 10)
|
||||
await dataService.close()
|
||||
}
|
||||
|
||||
func previewFor(_ session: HermesSession) -> String {
|
||||
if let title = session.title, !title.isEmpty { return title }
|
||||
if let preview = sessionPreviews[session.id], !preview.isEmpty { return preview }
|
||||
return session.id
|
||||
}
|
||||
|
||||
private func launchTerminal(arguments: [String]) {
|
||||
if let existing = terminalView {
|
||||
existing.terminate()
|
||||
|
||||
@@ -56,7 +56,16 @@ struct ChatView: View {
|
||||
Button {
|
||||
viewModel.resumeSession(session.id)
|
||||
} label: {
|
||||
Text("\(session.displayTitle) — \(session.id.prefix(16))")
|
||||
HStack {
|
||||
Text(viewModel.previewFor(session))
|
||||
.lineLimit(1)
|
||||
if let date = session.startedAt {
|
||||
Text("·")
|
||||
.foregroundStyle(.secondary)
|
||||
Text(date, style: .relative)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ final class DashboardViewModel {
|
||||
totalInputTokens: 0, totalOutputTokens: 0, totalCostUSD: 0
|
||||
)
|
||||
var recentSessions: [HermesSession] = []
|
||||
var sessionPreviews: [String: String] = [:]
|
||||
var config = HermesConfig.empty
|
||||
var gatewayState: GatewayState?
|
||||
var hermesRunning = false
|
||||
@@ -21,6 +22,7 @@ final class DashboardViewModel {
|
||||
if opened {
|
||||
stats = await dataService.fetchStats()
|
||||
recentSessions = await dataService.fetchSessions(limit: 5)
|
||||
sessionPreviews = await dataService.fetchSessionPreviews(limit: 5)
|
||||
await dataService.close()
|
||||
}
|
||||
config = fileService.loadConfig()
|
||||
|
||||
@@ -73,7 +73,7 @@ struct DashboardView: View {
|
||||
.buttonStyle(.link)
|
||||
}
|
||||
ForEach(viewModel.recentSessions) { session in
|
||||
SessionRow(session: session)
|
||||
SessionRow(session: session, preview: viewModel.sessionPreviews[session.id])
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
coordinator.selectedSessionId = session.id
|
||||
@@ -145,6 +145,7 @@ struct StatCard: View {
|
||||
|
||||
struct SessionRow: View {
|
||||
let session: HermesSession
|
||||
var preview: String?
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
@@ -152,7 +153,7 @@ struct SessionRow: View {
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 20)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(session.displayTitle)
|
||||
Text(preview ?? session.displayTitle)
|
||||
.lineLimit(1)
|
||||
if let date = session.startedAt {
|
||||
Text(date, style: .relative)
|
||||
|
||||
@@ -5,6 +5,7 @@ final class SessionsViewModel {
|
||||
private let dataService = HermesDataService()
|
||||
|
||||
var sessions: [HermesSession] = []
|
||||
var sessionPreviews: [String: String] = [:]
|
||||
var selectedSession: HermesSession?
|
||||
var messages: [HermesMessage] = []
|
||||
var searchText = ""
|
||||
@@ -15,6 +16,13 @@ final class SessionsViewModel {
|
||||
let opened = await dataService.open()
|
||||
guard opened else { return }
|
||||
sessions = await dataService.fetchSessions(limit: 500)
|
||||
sessionPreviews = await dataService.fetchSessionPreviews(limit: 500)
|
||||
}
|
||||
|
||||
func previewFor(_ session: HermesSession) -> String {
|
||||
if let title = session.title, !title.isEmpty { return title }
|
||||
if let preview = sessionPreviews[session.id], !preview.isEmpty { return preview }
|
||||
return session.id
|
||||
}
|
||||
|
||||
func selectSession(_ session: HermesSession) async {
|
||||
|
||||
@@ -3,6 +3,7 @@ import SwiftUI
|
||||
struct SessionDetailView: View {
|
||||
let session: HermesSession
|
||||
let messages: [HermesMessage]
|
||||
var preview: String?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
@@ -15,7 +16,7 @@ struct SessionDetailView: View {
|
||||
|
||||
private var sessionHeader: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(session.displayTitle)
|
||||
Text(preview ?? session.displayTitle)
|
||||
.font(.title3.bold())
|
||||
HStack(spacing: 16) {
|
||||
Label(session.source, systemImage: session.sourceIcon)
|
||||
|
||||
@@ -62,7 +62,7 @@ struct SessionsView: View {
|
||||
}
|
||||
} else {
|
||||
ForEach(viewModel.sessions) { session in
|
||||
SessionRow(session: session)
|
||||
SessionRow(session: session, preview: viewModel.previewFor(session))
|
||||
.tag(session.id)
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ struct SessionsView: View {
|
||||
@ViewBuilder
|
||||
private var sessionDetail: some View {
|
||||
if let session = viewModel.selectedSession {
|
||||
SessionDetailView(session: session, messages: viewModel.messages)
|
||||
SessionDetailView(session: session, messages: viewModel.messages, preview: viewModel.previewFor(session))
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
} else {
|
||||
ContentUnavailableView("Select a Session", systemImage: "bubble.left.and.bubble.right", description: Text("Choose a session from the list"))
|
||||
|
||||
Reference in New Issue
Block a user