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:
Alan Wizemann
2026-04-08 23:52:42 -04:00
parent 44afa8f53b
commit ad30c0a943
3 changed files with 44 additions and 8 deletions
@@ -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 = """
@@ -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()
}
@@ -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))