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:
Alan Wizemann
2026-03-31 02:58:55 -04:00
parent 0344ce2b98
commit 0a73aab825
8 changed files with 63 additions and 6 deletions
@@ -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()
+10 -1
View File
@@ -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"))