Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions assistant/src/daemon/ipc-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,8 @@ export interface ToolResult {
diff?: { filePath: string; oldContent: string; newContent: string; isNewFile: boolean };
status?: string;
sessionId?: string;
/** Base64-encoded image data extracted from contentBlocks (e.g. browser_screenshot). */
imageData?: string;
}

export interface ConfirmationRequest {
Expand Down
8 changes: 5 additions & 3 deletions assistant/src/daemon/session.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Anthropic from '@anthropic-ai/sdk';
import { v4 as uuid } from 'uuid';
import type { Message, ContentBlock } from '../providers/types.js';
import type { Message, ContentBlock, ImageContent } from '../providers/types.js';
import { INTERACTIVE_SURFACE_TYPES } from './ipc-protocol.js';
import type { ServerMessage, UsageStats, UserMessageAttachment, SurfaceType, SurfaceData, DynamicPageSurfaceData, FileUploadSurfaceData, UiSurfaceShow } from './ipc-protocol.js';
import { repairHistory, deepRepairHistory } from './history-repair.js';
Expand Down Expand Up @@ -590,10 +590,12 @@ export class Session {
case 'tool_output_chunk':
onEvent({ type: 'tool_output_chunk', chunk: event.chunk });
break;
case 'tool_result':
onEvent({ type: 'tool_result', toolName: '', result: event.content, isError: event.isError, diff: event.diff, status: event.status, sessionId: this.conversationId });
case 'tool_result': {
const imageBlock = event.contentBlocks?.find((b): b is ImageContent => b.type === 'image');
onEvent({ type: 'tool_result', toolName: '', result: event.content, isError: event.isError, diff: event.diff, status: event.status, sessionId: this.conversationId, imageData: imageBlock?.source.data });
Comment thread
siddseethepalli marked this conversation as resolved.
pendingToolResults.set(event.toolUseId, { content: event.content, isError: event.isError, contentBlocks: event.contentBlocks });
break;
}
case 'error':
if (isProviderOrderingError(event.error.message)) {
orderingErrorDetected = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,17 @@ struct ToolCallData: Identifiable, Equatable {
var result: String?
var isError: Bool
var isComplete: Bool
/// Base64-encoded image data from tool contentBlocks (e.g. browser_screenshot).
var imageData: String?

init(id: UUID = UUID(), toolName: String, inputSummary: String, result: String? = nil, isError: Bool = false, isComplete: Bool = false) {
init(id: UUID = UUID(), toolName: String, inputSummary: String, result: String? = nil, isError: Bool = false, isComplete: Bool = false, imageData: String? = nil) {
self.id = id
self.toolName = toolName
self.inputSummary = inputSummary
self.result = result
self.isError = isError
self.isComplete = isComplete
self.imageData = imageData
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,7 @@ final class ChatViewModel: ObservableObject {
messages[msgIndex].toolCalls[tcIndex].result = truncatedResult
messages[msgIndex].toolCalls[tcIndex].isError = msg.isError ?? false
messages[msgIndex].toolCalls[tcIndex].isComplete = true
messages[msgIndex].toolCalls[tcIndex].imageData = msg.imageData
}

case .uiSurfaceShow(let msg):
Expand Down
40 changes: 30 additions & 10 deletions clients/macos/vellum-assistant/Features/Chat/ToolCallChip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ struct ToolCallChip: View {
toolCall.toolName
}

private var hasExpandableContent: Bool {
toolCall.result != nil || toolCall.imageData != nil
}

var body: some View {
VStack(alignment: .leading, spacing: 0) {
// Chip header (always visible)
Button {
if toolCall.isComplete {
if toolCall.isComplete && hasExpandableContent {
withAnimation(VAnimation.fast) { isExpanded.toggle() }
}
} label: {
Expand Down Expand Up @@ -42,7 +46,7 @@ struct ToolCallChip: View {
ProgressView()
.scaleEffect(0.6)
.frame(width: 14, height: 14)
} else if toolCall.result != nil {
} else if hasExpandableContent {
// Chevron for expandable result
Image(systemName: isExpanded ? "chevron.down" : "chevron.right")
.font(.system(size: 9, weight: .semibold))
Expand All @@ -55,15 +59,31 @@ struct ToolCallChip: View {
.buttonStyle(.plain)

// Expanded result
if isExpanded, let result = toolCall.result {
ScrollView {
Text(result)
.font(VFont.monoSmall)
.foregroundColor(VColor.textSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
if isExpanded, hasExpandableContent {
VStack(alignment: .leading, spacing: VSpacing.sm) {
// Image preview (for browser_screenshot etc.)
if let imageData = toolCall.imageData,
let data = Data(base64Encoded: imageData),
let nsImage = NSImage(data: data) {
Comment thread
siddseethepalli marked this conversation as resolved.
Image(nsImage: nsImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity)
.clipShape(RoundedRectangle(cornerRadius: VRadius.sm))
}

// Text result
if let result = toolCall.result {
ScrollView {
Text(result)
.font(VFont.monoSmall)
.foregroundColor(VColor.textSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
}
.frame(maxHeight: 200)
}
}
.frame(maxHeight: 200)
.padding(.horizontal, VSpacing.sm)
.padding(.bottom, VSpacing.sm)
}
Expand Down
2 changes: 2 additions & 0 deletions clients/macos/vellum-assistant/IPC/IPCMessages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,8 @@ struct ToolResultMessage: Decodable, Sendable {
let diff: ConfirmationRequestMessage.ConfirmationDiffInfo?
let status: String?
let sessionId: String?
/// Base64-encoded image data from tool contentBlocks (e.g. browser_screenshot).
let imageData: String?
}

/// Follow-up suggestion response from daemon.
Expand Down