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
18 changes: 15 additions & 3 deletions assistant/src/daemon/ipc-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ export interface IpcBlobProbe {

// === Surface types ===

export type SurfaceType = 'card' | 'form' | 'list' | 'table' | 'confirmation' | 'dynamic_page' | 'file_upload' | 'browser_view';
export type SurfaceType = 'card' | 'form' | 'list' | 'table' | 'confirmation' | 'dynamic_page' | 'file_upload' | 'browser_view' | 'document_preview';

export const INTERACTIVE_SURFACE_TYPES: SurfaceType[] = ['form', 'confirmation', 'dynamic_page', 'file_upload'];

Expand Down Expand Up @@ -594,7 +594,13 @@ export interface BrowserViewSurfaceData {
pages?: Array<{ id: string; title: string; url: string; active: boolean }>;
}

export type SurfaceData = CardSurfaceData | FormSurfaceData | ListSurfaceData | TableSurfaceData | ConfirmationSurfaceData | DynamicPageSurfaceData | FileUploadSurfaceData | BrowserViewSurfaceData;
export interface DocumentPreviewSurfaceData {
title: string;
surfaceId: string; // the doc's real surfaceId, for focusing the panel
subtitle?: string;
}

export type SurfaceData = CardSurfaceData | FormSurfaceData | ListSurfaceData | TableSurfaceData | ConfirmationSurfaceData | DynamicPageSurfaceData | FileUploadSurfaceData | BrowserViewSurfaceData | DocumentPreviewSurfaceData;

export interface UiSurfaceAction {
type: 'ui_surface_action';
Expand Down Expand Up @@ -1555,6 +1561,11 @@ export interface UiSurfaceShowBrowserView extends UiSurfaceShowBase {
data: BrowserViewSurfaceData;
}

export interface UiSurfaceShowDocumentPreview extends UiSurfaceShowBase {
surfaceType: 'document_preview';
data: DocumentPreviewSurfaceData;
}

export type UiSurfaceShow =
| UiSurfaceShowCard
| UiSurfaceShowForm
Expand All @@ -1563,7 +1574,8 @@ export type UiSurfaceShow =
| UiSurfaceShowConfirmation
| UiSurfaceShowDynamicPage
| UiSurfaceShowFileUpload
| UiSurfaceShowBrowserView;
| UiSurfaceShowBrowserView
| UiSurfaceShowDocumentPreview;

export interface UiSurfaceUpdate {
type: 'ui_surface_update';
Expand Down
14 changes: 14 additions & 0 deletions assistant/src/tools/document/document-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ export class DocumentCreateTool implements Tool {
initialContent,
});

context.sendToClient({
type: 'ui_surface_show',
sessionId: context.sessionId,
surfaceId: `preview-${surfaceId}`,
surfaceType: 'document_preview',
display: 'inline',
Comment thread
marinatrajk marked this conversation as resolved.
title,
data: {
title,
surfaceId,
subtitle: 'Document',
},
});

return {
content: JSON.stringify({
surface_id: surfaceId,
Expand Down
1 change: 1 addition & 0 deletions clients/macos/vellum-assistant/App/APIKeyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ extension Notification.Name {
static let openDynamicWorkspace = Notification.Name("MainWindow.openDynamicWorkspace")
static let updateDynamicWorkspace = Notification.Name("MainWindow.updateDynamicWorkspace")
static let dismissDynamicWorkspace = Notification.Name("MainWindow.dismissDynamicWorkspace")
static let openDocumentEditor = Notification.Name("MainWindow.openDocumentEditor")
}

/// Manages API keys in the macOS login keychain.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,9 @@ struct MainWindowView: View {
}
}
}
.onReceive(NotificationCenter.default.publisher(for: .openDocumentEditor)) { _ in
windowState.selection = .panel(.documentEditor)
Comment thread
marinatrajk marked this conversation as resolved.
}
.onReceive(NotificationCenter.default.publisher(for: .updateDynamicWorkspace)) { notification in
if let updated = notification.userInfo?["surface"] as? Surface,
updated.id == windowState.activeDynamicSurface?.surfaceId {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import SwiftUI

/// Compact preview card for documents shown inline in chat.
/// The entire card is clickable to open the document editor panel.
public struct InlineDocumentPreview: View {
public let data: DocumentPreviewSurfaceData
public let onOpen: () -> Void

public init(data: DocumentPreviewSurfaceData, onOpen: @escaping () -> Void) {
self.data = data
self.onOpen = onOpen
}

public var body: some View {
Button {
onOpen()
} label: {
HStack(spacing: VSpacing.sm) {
Image(systemName: "doc.text")
.font(.system(size: 20, weight: .medium))
.foregroundColor(VColor.accent)

VStack(alignment: .leading, spacing: VSpacing.xxs) {
Text(data.title)
.font(VFont.bodyBold)
.foregroundColor(VColor.textPrimary)
.lineLimit(2)

if let subtitle = data.subtitle {
Text(subtitle)
.font(VFont.caption)
.foregroundColor(VColor.textMuted)
.lineLimit(1)
}
}

Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityLabel("Open document: \(data.title)")
.accessibilityAddTraits(.isButton)
}
}

#if DEBUG
#Preview("InlineDocumentPreview") {
ZStack {
VColor.background.ignoresSafeArea()
InlineDocumentPreview(
data: DocumentPreviewSurfaceData(
title: "Blog Post: The Future of Swift",
surfaceId: "doc-preview-123",
subtitle: "Document"
),
onOpen: {}
)
.padding()
}
.frame(width: 400, height: 120)
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,18 @@ public struct InlineSurfaceRouter: View {
return false
}

private var isDocumentPreview: Bool {
if case .documentPreview = surface.data { return true }
return false
}

public var body: some View {
if let completion = surface.completionState {
CompletedSurfaceChip(title: surface.title, summary: completion.summary)
} else {
VStack(alignment: .leading, spacing: VSpacing.sm) {
// Template cards and dynamic page previews handle their own header
if !isTemplateCard, !isDynamicPreview, let title = surface.title {
if !isTemplateCard, !isDynamicPreview, !isDocumentPreview, let title = surface.title {
Text(title)
.font(VFont.cardTitle)
.foregroundColor(VColor.textPrimary)
Expand All @@ -47,7 +52,7 @@ public struct InlineSurfaceRouter: View {
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.inlineWidgetCard(interactive: isDynamicPreview)
.inlineWidgetCard(interactive: isDynamicPreview || isDocumentPreview)
.overlay(alignment: .topTrailing) {
if isDynamicPreview {
Button {
Expand All @@ -70,10 +75,31 @@ public struct InlineSurfaceRouter: View {
}
.buttonStyle(.plain)
.padding(VSpacing.sm)
} else if isDocumentPreview {
if case .documentPreview(let data) = surface.data {
Button {
NotificationCenter.default.post(
name: Notification.Name("MainWindow.openDocumentEditor"),
object: nil,
userInfo: ["documentSurfaceId": data.surfaceId]
)
} label: {
Image(systemName: "arrow.up.right")
.font(.system(size: 10, weight: .semibold))
.foregroundColor(VColor.textSecondary)
.padding(VSpacing.xs)
.background(
RoundedRectangle(cornerRadius: VRadius.sm)
.fill(VColor.surfaceBorder.opacity(0.3))
)
}
.buttonStyle(.plain)
.padding(VSpacing.sm)
}
}
}
// Consistent width for all widget cards; dynamic page previews are more compact.
.frame(maxWidth: isDynamicPreview ? 350 : 540, alignment: .leading)
// Consistent width for all widget cards; dynamic page previews and document previews are more compact.
.frame(maxWidth: isDynamicPreview || isDocumentPreview ? 350 : 540, alignment: .leading)
}
}

Expand All @@ -82,6 +108,14 @@ public struct InlineSurfaceRouter: View {
switch surface.data {
case .card(let data):
InlineCardWidget(data: data)
case .documentPreview(let data):
InlineDocumentPreview(data: data) {
NotificationCenter.default.post(
name: Notification.Name("MainWindow.openDocumentEditor"),
object: nil,
userInfo: ["documentSurfaceId": data.surfaceId]
)
}
case .dynamicPage(let data):
if let preview = data.preview {
InlineDynamicPagePreview(preview: preview) {
Expand Down
24 changes: 24 additions & 0 deletions clients/shared/Features/Surfaces/SurfaceTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum SurfaceType: String, Codable, Sendable {
case dynamicPage = "dynamic_page"
case fileUpload = "file_upload"
case browserView = "browser_view"
case documentPreview = "document_preview"
}

public enum SurfaceActionStyle: String, Codable, Sendable {
Expand Down Expand Up @@ -301,6 +302,18 @@ public struct BrowserPage: Sendable, Identifiable {
}
}

public struct DocumentPreviewSurfaceData: Sendable {
public let title: String
public let surfaceId: String
public let subtitle: String?

public init(title: String, surfaceId: String, subtitle: String? = nil) {
self.title = title
self.surfaceId = surfaceId
self.subtitle = subtitle
}
}

public struct BrowserViewSurfaceData: Sendable {
public let sessionId: String
public let currentUrl: String
Expand Down Expand Up @@ -370,6 +383,7 @@ public enum SurfaceData: Sendable {
case dynamicPage(DynamicPageSurfaceData)
case fileUpload(FileUploadSurfaceData)
case browserView(BrowserViewSurfaceData)
case documentPreview(DocumentPreviewSurfaceData)
}

public struct SurfaceActionButton: Identifiable, Equatable, Sendable {
Expand Down Expand Up @@ -506,6 +520,8 @@ public extension Surface {
return parseFileUploadData(dict).map { .fileUpload($0) }
case .browserView:
return parseBrowserViewData(dict).map { .browserView($0) }
case .documentPreview:
return parseDocumentPreviewData(dict).map { .documentPreview($0) }
}
}

Expand All @@ -532,6 +548,8 @@ public extension Surface {
return .fileUpload(mergeFileUploadData(existing: fu, update: update))
case .browserView(let bv):
return .browserView(mergeBrowserViewData(existing: bv, update: update))
case .documentPreview(let dp):
return .documentPreview(dp)
}
}

Expand Down Expand Up @@ -997,6 +1015,12 @@ public extension Surface {
)
}

private static func parseDocumentPreviewData(_ dict: [String: Any?]) -> DocumentPreviewSurfaceData? {
guard let title = dict["title"] as? String,
let surfaceId = dict["surfaceId"] as? String else { return nil }
return DocumentPreviewSurfaceData(title: title, surfaceId: surfaceId, subtitle: dict["subtitle"] as? String)
}

private static func mergeBrowserViewData(existing: BrowserViewSurfaceData, update: [String: Any?]) -> BrowserViewSurfaceData {
let sessionId = (update["sessionId"] as? String) ?? existing.sessionId
let currentUrl = (update["currentUrl"] as? String) ?? existing.currentUrl
Expand Down
Loading