mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
feat: Show tool output in Activity inspector (#12)
Add tool result display to the Activity detail pane. When selecting a tool call, the inspector now shows Arguments → Output → Assistant Message, giving full visibility into what was requested, what came back, and how the assistant interpreted it. - Add fetchToolResult(callId:) query to HermesDataService - Fetch tool result on entry selection in ActivityViewModel - Display output in styled monospaced box in detail pane - Render assistant message with MarkdownContentView Closes #12 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -157,6 +157,17 @@ actor HermesDataService {
|
|||||||
return messages
|
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] {
|
func fetchRecentToolCalls(limit: Int = QueryDefaults.toolCallLimit) -> [HermesMessage] {
|
||||||
guard let db else { return [] }
|
guard let db else { return [] }
|
||||||
let sql = """
|
let sql = """
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ final class ActivityViewModel {
|
|||||||
var filterKind: ToolKind?
|
var filterKind: ToolKind?
|
||||||
var filterSessionId: String?
|
var filterSessionId: String?
|
||||||
var selectedEntry: ActivityEntry?
|
var selectedEntry: ActivityEntry?
|
||||||
|
var toolResult: String?
|
||||||
var sessionPreviews: [String: String] = [:]
|
var sessionPreviews: [String: String] = [:]
|
||||||
var isLoading = true
|
var isLoading = true
|
||||||
|
|
||||||
@@ -54,6 +55,15 @@ final class ActivityViewModel {
|
|||||||
isLoading = false
|
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 {
|
func cleanup() async {
|
||||||
await dataService.close()
|
await dataService.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,11 +57,8 @@ struct ActivityView: View {
|
|||||||
List(selection: Binding(
|
List(selection: Binding(
|
||||||
get: { viewModel.selectedEntry?.id },
|
get: { viewModel.selectedEntry?.id },
|
||||||
set: { id in
|
set: { id in
|
||||||
if let id {
|
let entry = id.flatMap { id in viewModel.filteredActivity.first(where: { $0.id == id }) }
|
||||||
viewModel.selectedEntry = viewModel.filteredActivity.first(where: { $0.id == id })
|
Task { await viewModel.selectEntry(entry) }
|
||||||
} else {
|
|
||||||
viewModel.selectedEntry = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)) {
|
)) {
|
||||||
ForEach(viewModel.filteredActivity) { entry in
|
ForEach(viewModel.filteredActivity) { entry in
|
||||||
@@ -146,14 +143,32 @@ struct ActivityView: View {
|
|||||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
.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 {
|
if !entry.messageContent.isEmpty {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Assistant Message")
|
Text("Assistant Message")
|
||||||
.font(.caption.bold())
|
.font(.caption.bold())
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Text(entry.messageContent)
|
MarkdownContentView(content: entry.messageContent)
|
||||||
.font(.caption)
|
|
||||||
.textSelection(.enabled)
|
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.background(.quaternary.opacity(0.5))
|
.background(.quaternary.opacity(0.5))
|
||||||
|
|||||||
Reference in New Issue
Block a user