diff --git a/scarf/scarf/Core/Services/HermesDataService.swift b/scarf/scarf/Core/Services/HermesDataService.swift index ad384c7..a71cea8 100644 --- a/scarf/scarf/Core/Services/HermesDataService.swift +++ b/scarf/scarf/Core/Services/HermesDataService.swift @@ -157,6 +157,17 @@ actor HermesDataService { return messages } + func fetchToolResult(callId: String) -> String? { + guard let db else { return nil } + let sql = "SELECT content FROM messages WHERE role = 'tool' AND tool_call_id = ? LIMIT 1" + var stmt: OpaquePointer? + guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else { return nil } + defer { sqlite3_finalize(stmt) } + sqlite3_bind_text(stmt, 1, callId, -1, sqliteTransient) + guard sqlite3_step(stmt) == SQLITE_ROW else { return nil } + return columnText(stmt!, 0) + } + func fetchRecentToolCalls(limit: Int = QueryDefaults.toolCallLimit) -> [HermesMessage] { guard let db else { return [] } let sql = """ diff --git a/scarf/scarf/Features/Activity/ViewModels/ActivityViewModel.swift b/scarf/scarf/Features/Activity/ViewModels/ActivityViewModel.swift index dc1d4f2..6c7a22e 100644 --- a/scarf/scarf/Features/Activity/ViewModels/ActivityViewModel.swift +++ b/scarf/scarf/Features/Activity/ViewModels/ActivityViewModel.swift @@ -8,6 +8,7 @@ final class ActivityViewModel { var filterKind: ToolKind? var filterSessionId: String? var selectedEntry: ActivityEntry? + var toolResult: String? var sessionPreviews: [String: String] = [:] var isLoading = true @@ -54,6 +55,15 @@ final class ActivityViewModel { isLoading = false } + func selectEntry(_ entry: ActivityEntry?) async { + selectedEntry = entry + if let entry { + toolResult = await dataService.fetchToolResult(callId: entry.id) + } else { + toolResult = nil + } + } + func cleanup() async { await dataService.close() } diff --git a/scarf/scarf/Features/Activity/Views/ActivityView.swift b/scarf/scarf/Features/Activity/Views/ActivityView.swift index a5d4d00..7618fb7 100644 --- a/scarf/scarf/Features/Activity/Views/ActivityView.swift +++ b/scarf/scarf/Features/Activity/Views/ActivityView.swift @@ -57,11 +57,8 @@ struct ActivityView: View { List(selection: Binding( get: { viewModel.selectedEntry?.id }, set: { id in - if let id { - viewModel.selectedEntry = viewModel.filteredActivity.first(where: { $0.id == id }) - } else { - viewModel.selectedEntry = nil - } + let entry = id.flatMap { id in viewModel.filteredActivity.first(where: { $0.id == id }) } + Task { await viewModel.selectEntry(entry) } } )) { ForEach(viewModel.filteredActivity) { entry in @@ -146,14 +143,32 @@ struct ActivityView: View { .clipShape(RoundedRectangle(cornerRadius: 6)) } + if let result = viewModel.toolResult, !result.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Text("Output") + .font(.caption.bold()) + .foregroundStyle(.secondary) + Text(result) + .font(.system(.caption, design: .monospaced)) + .textSelection(.enabled) + .lineLimit(50) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(.textBackgroundColor).opacity(0.5)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .overlay( + RoundedRectangle(cornerRadius: 6) + .strokeBorder(.quaternary, lineWidth: 1) + ) + } + } + if !entry.messageContent.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("Assistant Message") .font(.caption.bold()) .foregroundStyle(.secondary) - Text(entry.messageContent) - .font(.caption) - .textSelection(.enabled) + MarkdownContentView(content: entry.messageContent) .padding(8) .frame(maxWidth: .infinity, alignment: .leading) .background(.quaternary.opacity(0.5))