From 0a73aab825eb3cdbaf0c0c831fbb4f3dae4e476e Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Tue, 31 Mar 2026 02:58:55 -0400 Subject: [PATCH] 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) --- .../Core/Services/HermesDataService.swift | 28 +++++++++++++++++++ .../Chat/ViewModels/ChatViewModel.swift | 8 ++++++ .../scarf/Features/Chat/Views/ChatView.swift | 11 +++++++- .../ViewModels/DashboardViewModel.swift | 2 ++ .../Dashboard/Views/DashboardView.swift | 5 ++-- .../ViewModels/SessionsViewModel.swift | 8 ++++++ .../Sessions/Views/SessionDetailView.swift | 3 +- .../Sessions/Views/SessionsView.swift | 4 +-- 8 files changed, 63 insertions(+), 6 deletions(-) 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"))