diff --git a/scarf/scarf/Core/Services/HermesDataService.swift b/scarf/scarf/Core/Services/HermesDataService.swift index f40f864..f7f0793 100644 --- a/scarf/scarf/Core/Services/HermesDataService.swift +++ b/scarf/scarf/Core/Services/HermesDataService.swift @@ -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 diff --git a/scarf/scarf/Features/Chat/ViewModels/ChatViewModel.swift b/scarf/scarf/Features/Chat/ViewModels/ChatViewModel.swift index d9934e5..261088b 100644 --- a/scarf/scarf/Features/Chat/ViewModels/ChatViewModel.swift +++ b/scarf/scarf/Features/Chat/ViewModels/ChatViewModel.swift @@ -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() diff --git a/scarf/scarf/Features/Chat/Views/ChatView.swift b/scarf/scarf/Features/Chat/Views/ChatView.swift index 194a037..b28a8f5 100644 --- a/scarf/scarf/Features/Chat/Views/ChatView.swift +++ b/scarf/scarf/Features/Chat/Views/ChatView.swift @@ -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) + } + } } } } diff --git a/scarf/scarf/Features/Dashboard/ViewModels/DashboardViewModel.swift b/scarf/scarf/Features/Dashboard/ViewModels/DashboardViewModel.swift index 7a6f464..fbaf879 100644 --- a/scarf/scarf/Features/Dashboard/ViewModels/DashboardViewModel.swift +++ b/scarf/scarf/Features/Dashboard/ViewModels/DashboardViewModel.swift @@ -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() diff --git a/scarf/scarf/Features/Dashboard/Views/DashboardView.swift b/scarf/scarf/Features/Dashboard/Views/DashboardView.swift index 3830d7f..0e698b8 100644 --- a/scarf/scarf/Features/Dashboard/Views/DashboardView.swift +++ b/scarf/scarf/Features/Dashboard/Views/DashboardView.swift @@ -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) diff --git a/scarf/scarf/Features/Sessions/ViewModels/SessionsViewModel.swift b/scarf/scarf/Features/Sessions/ViewModels/SessionsViewModel.swift index a71f0e4..72ab51b 100644 --- a/scarf/scarf/Features/Sessions/ViewModels/SessionsViewModel.swift +++ b/scarf/scarf/Features/Sessions/ViewModels/SessionsViewModel.swift @@ -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 { diff --git a/scarf/scarf/Features/Sessions/Views/SessionDetailView.swift b/scarf/scarf/Features/Sessions/Views/SessionDetailView.swift index 0cae7fe..9454531 100644 --- a/scarf/scarf/Features/Sessions/Views/SessionDetailView.swift +++ b/scarf/scarf/Features/Sessions/Views/SessionDetailView.swift @@ -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) diff --git a/scarf/scarf/Features/Sessions/Views/SessionsView.swift b/scarf/scarf/Features/Sessions/Views/SessionsView.swift index bb0f6f5..44f904f 100644 --- a/scarf/scarf/Features/Sessions/Views/SessionsView.swift +++ b/scarf/scarf/Features/Sessions/Views/SessionsView.swift @@ -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"))