mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 02:26:37 +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
|
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()
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
Reference in New Issue
Block a user