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
1 change: 1 addition & 0 deletions assistant/src/calls/guardian-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export async function dispatchGuardianQuestion(params: GuardianDispatchParams):
requestId: request.id,
callSessionId,
title: guardianCopy.threadTitle,
questionText: request.questionText,
} as ServerMessage);
}
updateDeliveryStatus(delivery.id, 'sent');
Expand Down
1 change: 1 addition & 0 deletions assistant/src/daemon/ipc-contract/work-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,5 @@ export interface GuardianRequestThreadCreated {
requestId: string;
callSessionId: string;
title: string;
questionText: string;
}
26 changes: 24 additions & 2 deletions clients/macos/vellum-assistant/App/AppDelegate+Notifications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,19 @@ extension AppDelegate {
options: []
)

center.setNotificationCategories([activityCategory, toolConfirmationCategory, rideShotgunCategory, voiceResponseCategory, quickChatCategory])
let viewGuardianAction = UNNotificationAction(
identifier: "VIEW_GUARDIAN",
title: "View",
options: [.foreground]
)
let guardianRequestCategory = UNNotificationCategory(
identifier: "GUARDIAN_REQUEST",
actions: [viewGuardianAction],
intentIdentifiers: [],
options: []
)

center.setNotificationCategories([activityCategory, toolConfirmationCategory, rideShotgunCategory, voiceResponseCategory, quickChatCategory, guardianRequestCategory])
}

func registerBundledFonts() {
Expand Down Expand Up @@ -165,7 +177,17 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
let conversationId = response.notification.request.content.userInfo["conversationId"] as? String
await MainActor.run {
guard !self.isAwaitingFirstLaunchReady else { return }
self.openQuickChatThread(conversationId: conversationId)
self.openConversationThread(conversationId: conversationId)
}
return
}

// Handle guardian request notifications — open the guardian thread in the main window
if categoryId == "GUARDIAN_REQUEST" {
let conversationId = response.notification.request.content.userInfo["conversationId"] as? String
await MainActor.run {
guard !self.isAwaitingFirstLaunchReady else { return }
self.openConversationThread(conversationId: conversationId)
}
return
}
Expand Down
23 changes: 22 additions & 1 deletion clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -402,9 +402,30 @@ extension AppDelegate {
}
}

func deliverGuardianRequestNotification(title: String, questionText: String, conversationId: String) {
let content = UNMutableNotificationContent()
content.title = title
content.body = String(questionText.prefix(200))
content.sound = .default
content.categoryIdentifier = "GUARDIAN_REQUEST"
content.userInfo = ["conversationId": conversationId]

let request = UNNotificationRequest(
identifier: "guardian-request-\(conversationId)",
content: content,
trigger: nil
)
UNUserNotificationCenter.current().add(request) { error in
if let error {
log.error("Failed to post guardian request notification: \(error.localizedDescription)")
}
}
}

/// Opens the main window and navigates to the thread for the given conversation ID.
/// Retries if the thread isn't populated yet (e.g., ThreadManager hasn't loaded it).
func openQuickChatThread(conversationId: String?) {
/// Used by Quick Chat, Guardian Request, and other notification deep links.
func openConversationThread(conversationId: String?) {
showMainWindow()
guard let conversationId else { return }

Expand Down
16 changes: 13 additions & 3 deletions clients/macos/vellum-assistant/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -685,10 +685,20 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
callSessionId: msg.callSessionId,
title: msg.title
)
if let thread = self.mainWindow?.threadManager.threads.first(where: { $0.sessionId == msg.conversationId }) {
self.mainWindow?.threadManager.activeThreadId = thread.id
if NSApp.isActive {
// App is in foreground — select thread and show window immediately
if let thread = self.mainWindow?.threadManager.threads.first(where: { $0.sessionId == msg.conversationId }) {
self.mainWindow?.threadManager.activeThreadId = thread.id
}
self.showMainWindow()
} else {
// App is backgrounded — post native notification
self.deliverGuardianRequestNotification(
title: msg.title,
questionText: msg.questionText,
conversationId: msg.conversationId
Comment thread
noanflaherty marked this conversation as resolved.
Comment thread
noanflaherty marked this conversation as resolved.
)
}
self.showMainWindow()
}

// Handle escalation: text_qa -> computer_use via computer_use_request_control
Expand Down
4 changes: 3 additions & 1 deletion clients/shared/IPC/Generated/IPCContractGenerated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1853,13 +1853,15 @@ public struct IPCGuardianRequestThreadCreated: Codable, Sendable {
public let requestId: String
public let callSessionId: String
public let title: String
public let questionText: String

public init(type: String, conversationId: String, requestId: String, callSessionId: String, title: String) {
public init(type: String, conversationId: String, requestId: String, callSessionId: String, title: String, questionText: String) {
self.type = type
self.conversationId = conversationId
self.requestId = requestId
self.callSessionId = callSessionId
self.title = title
self.questionText = questionText
}
}

Expand Down
Loading