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 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 { struct SessionStats: Sendable {
let totalSessions: Int let totalSessions: Int
let totalMessages: Int let totalMessages: Int
@@ -7,6 +7,7 @@ final class ChatViewModel {
private let dataService = HermesDataService() private let dataService = HermesDataService()
var recentSessions: [HermesSession] = [] var recentSessions: [HermesSession] = []
var sessionPreviews: [String: String] = [:]
var terminalView: LocalProcessTerminalView? var terminalView: LocalProcessTerminalView?
var hasActiveProcess = false var hasActiveProcess = false
private var coordinator: Coordinator? private var coordinator: Coordinator?
@@ -31,9 +32,16 @@ final class ChatViewModel {
let opened = await dataService.open() let opened = await dataService.open()
guard opened else { return } guard opened else { return }
recentSessions = await dataService.fetchSessions(limit: 10) recentSessions = await dataService.fetchSessions(limit: 10)
sessionPreviews = await dataService.fetchSessionPreviews(limit: 10)
await dataService.close() 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]) { private func launchTerminal(arguments: [String]) {
if let existing = terminalView { if let existing = terminalView {
existing.terminate() existing.terminate()
+10 -1
View File
@@ -56,7 +56,16 @@ struct ChatView: View {
Button { Button {
viewModel.resumeSession(session.id) viewModel.resumeSession(session.id)
} label: { } 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 totalInputTokens: 0, totalOutputTokens: 0, totalCostUSD: 0
) )
var recentSessions: [HermesSession] = [] var recentSessions: [HermesSession] = []
var sessionPreviews: [String: String] = [:]
var config = HermesConfig.empty var config = HermesConfig.empty
var gatewayState: GatewayState? var gatewayState: GatewayState?
var hermesRunning = false var hermesRunning = false
@@ -21,6 +22,7 @@ final class DashboardViewModel {
if opened { if opened {
stats = await dataService.fetchStats() stats = await dataService.fetchStats()
recentSessions = await dataService.fetchSessions(limit: 5) recentSessions = await dataService.fetchSessions(limit: 5)
sessionPreviews = await dataService.fetchSessionPreviews(limit: 5)
await dataService.close() await dataService.close()
} }
config = fileService.loadConfig() config = fileService.loadConfig()
@@ -73,7 +73,7 @@ struct DashboardView: View {
.buttonStyle(.link) .buttonStyle(.link)
} }
ForEach(viewModel.recentSessions) { session in ForEach(viewModel.recentSessions) { session in
SessionRow(session: session) SessionRow(session: session, preview: viewModel.sessionPreviews[session.id])
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .onTapGesture {
coordinator.selectedSessionId = session.id coordinator.selectedSessionId = session.id
@@ -145,6 +145,7 @@ struct StatCard: View {
struct SessionRow: View { struct SessionRow: View {
let session: HermesSession let session: HermesSession
var preview: String?
var body: some View { var body: some View {
HStack { HStack {
@@ -152,7 +153,7 @@ struct SessionRow: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.frame(width: 20) .frame(width: 20)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(session.displayTitle) Text(preview ?? session.displayTitle)
.lineLimit(1) .lineLimit(1)
if let date = session.startedAt { if let date = session.startedAt {
Text(date, style: .relative) Text(date, style: .relative)
@@ -5,6 +5,7 @@ final class SessionsViewModel {
private let dataService = HermesDataService() private let dataService = HermesDataService()
var sessions: [HermesSession] = [] var sessions: [HermesSession] = []
var sessionPreviews: [String: String] = [:]
var selectedSession: HermesSession? var selectedSession: HermesSession?
var messages: [HermesMessage] = [] var messages: [HermesMessage] = []
var searchText = "" var searchText = ""
@@ -15,6 +16,13 @@ final class SessionsViewModel {
let opened = await dataService.open() let opened = await dataService.open()
guard opened else { return } guard opened else { return }
sessions = await dataService.fetchSessions(limit: 500) 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 { func selectSession(_ session: HermesSession) async {
@@ -3,6 +3,7 @@ import SwiftUI
struct SessionDetailView: View { struct SessionDetailView: View {
let session: HermesSession let session: HermesSession
let messages: [HermesMessage] let messages: [HermesMessage]
var preview: String?
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
@@ -15,7 +16,7 @@ struct SessionDetailView: View {
private var sessionHeader: some View { private var sessionHeader: some View {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
Text(session.displayTitle) Text(preview ?? session.displayTitle)
.font(.title3.bold()) .font(.title3.bold())
HStack(spacing: 16) { HStack(spacing: 16) {
Label(session.source, systemImage: session.sourceIcon) Label(session.source, systemImage: session.sourceIcon)
@@ -62,7 +62,7 @@ struct SessionsView: View {
} }
} else { } else {
ForEach(viewModel.sessions) { session in ForEach(viewModel.sessions) { session in
SessionRow(session: session) SessionRow(session: session, preview: viewModel.previewFor(session))
.tag(session.id) .tag(session.id)
} }
} }
@@ -73,7 +73,7 @@ struct SessionsView: View {
@ViewBuilder @ViewBuilder
private var sessionDetail: some View { private var sessionDetail: some View {
if let session = viewModel.selectedSession { 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) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
} else { } else {
ContentUnavailableView("Select a Session", systemImage: "bubble.left.and.bubble.right", description: Text("Choose a session from the list")) ContentUnavailableView("Select a Session", systemImage: "bubble.left.and.bubble.right", description: Text("Choose a session from the list"))