diff --git a/clients/.gitignore b/clients/.gitignore new file mode 100644 index 00000000000..9f897360ebd --- /dev/null +++ b/clients/.gitignore @@ -0,0 +1,12 @@ +.build/ +build/ +dist/ +.swiftpm/ +*.xcuserdata +xcuserdata/ +DerivedData/ +.dev/ +.DS_Store +context.md +Local.xcconfig +daemon-bin/ diff --git a/clients/Package.resolved b/clients/Package.resolved new file mode 100644 index 00000000000..dd4bd7afab2 --- /dev/null +++ b/clients/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "hotkey", + "kind" : "remoteSourceControl", + "location" : "https://github.com/soffes/HotKey", + "state" : { + "revision" : "a3cf605d7a96f6ff50e04fcb6dea6e2613cfcbe4", + "version" : "0.2.1" + } + }, + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle", + "state" : { + "revision" : "5581748cef2bae787496fe6d61139aebe0a451f6", + "version" : "2.8.1" + } + } + ], + "version" : 2 +} diff --git a/clients/macos/Package.swift b/clients/Package.swift similarity index 65% rename from clients/macos/Package.swift rename to clients/Package.swift index ea040c18e59..810f2181217 100644 --- a/clients/macos/Package.swift +++ b/clients/Package.swift @@ -4,13 +4,18 @@ import PackageDescription let package = Package( name: "vellum-assistant", platforms: [ - .macOS(.v14) + .macOS(.v14), + .iOS(.v17) ], products: [ .library( name: "VellumAssistantLib", targets: ["VellumAssistantLib"] ), + .library( + name: "VellumAssistantShared", + targets: ["VellumAssistantShared"] + ), .executable( name: "vellum-assistant", targets: ["vellum-assistant"] @@ -21,10 +26,23 @@ let package = Package( .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"), ], targets: [ + .target( + name: "VellumAssistantShared", + dependencies: [], + path: "shared", + swiftSettings: [ + .enableUpcomingFeature("BareSlashRegexLiterals") + ], + linkerSettings: [ + .linkedFramework("Network") // Required for DaemonClient (NWConnection) + ] + ), + // VellumAssistantLib: macOS-only target (links AppKit, ScreenCaptureKit, etc.) + // iOS apps should depend only on VellumAssistantShared, not this target. .target( name: "VellumAssistantLib", - dependencies: ["HotKey", "Sparkle"], - path: "vellum-assistant", + dependencies: ["VellumAssistantShared", "HotKey", "Sparkle"], + path: "macos/vellum-assistant", exclude: ["Resources/Info.plist"], resources: [ .process("Resources/Assets.xcassets"), @@ -49,12 +67,12 @@ let package = Package( .executableTarget( name: "vellum-assistant", dependencies: ["VellumAssistantLib"], - path: "vellum-assistant-app" + path: "macos/vellum-assistant-app" ), .testTarget( name: "vellum-assistantTests", dependencies: ["VellumAssistantLib"], - path: "vellum-assistantTests" + path: "macos/vellum-assistantTests" ) ] ) diff --git a/clients/README.md b/clients/README.md new file mode 100644 index 00000000000..13ad5cce38d --- /dev/null +++ b/clients/README.md @@ -0,0 +1,153 @@ +# Clients Directory + +This directory contains native client applications for the Vellum Assistant, organized for code reuse between platforms. + +## Structure + +``` +clients/ +├── Package.swift # Multi-platform Swift Package Manager manifest +├── shared/ # VellumAssistantShared - cross-platform code +│ ├── IPC/ # Daemon communication (both macOS and iOS) +│ └── App/ # Shared app utilities +├── macos/ # macOS-specific code +│ ├── vellum-assistant/ # VellumAssistantLib - macOS app logic +│ ├── vellum-assistant-app/ # Executable entry point +│ ├── build.sh # Build script (wraps SPM → .app → codesign) +│ └── CLAUDE.md # Development guide for Claude Code +└── ios/ # (Future) iOS-specific code + └── vellum-assistant-ios/ # iOS app (planned in PR 4-6) +``` + +## Targets + +### VellumAssistantShared (Library) +**Platforms**: macOS 14+, iOS 17+ +**Purpose**: Platform-agnostic code shared between macOS and iOS apps + +**Contains**: +- **IPC layer** (`DaemonClient`, `IPCMessages`) - Network communication with daemon + - macOS: Unix domain socket (`~/.vellum/vellum.sock`) + - iOS: TCP connection (configurable hostname:port) +- **Shared utilities** (signing, configuration) + +**Dependencies**: None (only system frameworks: Network) + +### VellumAssistantLib (Library) +**Platforms**: macOS 14+ only +**Purpose**: macOS application logic + +**Contains**: +- UI (AppKit views, panels, overlays) +- Computer-use features (accessibility, screen capture, input injection) +- macOS-specific integrations (menu bar, hotkeys, voice input) + +**Dependencies**: VellumAssistantShared, HotKey, Sparkle +**Frameworks**: AppKit, ScreenCaptureKit, ApplicationServices, Vision, Speech + +**⚠️ iOS apps should NOT depend on this target** - it links macOS-only frameworks. + +### vellum-assistant (Executable) +**Platforms**: macOS 14+ +**Purpose**: Thin entry point for macOS app + +**Contains**: Just `@main` app delegate setup +**Dependencies**: VellumAssistantLib + +## Building + +### macOS App +```bash +cd clients/macos +./build.sh # Build debug .app +./build.sh run # Build + launch +./build.sh release # Build release +./build.sh test # Run tests +./build.sh clean # Remove artifacts +``` + +The build script: +1. Runs `swift build` from `clients/macos/` (SPM finds `../Package.swift` automatically) +2. Packages binary into `dist/Vellum.app` bundle +3. Codesigns with ad-hoc signature (or release identity) + +### iOS App (Future) +Planned for PR 4-6 of the iOS rollout. Will depend only on VellumAssistantShared. + +## Code Reuse Strategy + +**~45-50% code reuse** between macOS and iOS achieved through: + +1. **Shared IPC layer** - Both platforms communicate with daemon (different transport) +2. **Shared design system** (PR 2) - Tokens and components with conditional compilation +3. **Shared ViewModels** (PR 3) - ChatViewModel, message models work on both platforms + +**Platform-specific**: +- **UI frameworks**: AppKit (macOS) vs UIKit (iOS) +- **Computer-use**: AXUIElement + CGEvent (macOS only, sandboxing prevents on iOS) +- **Screen recording**: ScreenCaptureKit (macOS) vs ReplayKit (iOS) +- **App lifecycle**: NSStatusItem (macOS) vs UIScene (iOS) + +## Migration from Single-Platform + +This structure was introduced in PR #1821 (iOS shared library foundation). Before this: +- `clients/macos/Package.swift` - Single-platform package +- `clients/macos/vellum-assistant/IPC/` - macOS-only IPC code + +After migration: +- `clients/Package.swift` - Multi-platform package +- `clients/shared/IPC/` - Cross-platform IPC code +- All 25+ IPC message types have `public` access and explicit `public init()` + +## Development + +### Adding Shared Code +1. Place platform-agnostic code in `clients/shared/` +2. Mark all types as `public` (cross-module access) +3. Add explicit `public init()` to all structs (memberwise inits are internal) +4. Use `#if os(macOS)` / `#elseif os(iOS)` for platform-specific code + +### Adding macOS-Only Code +1. Place in `clients/macos/vellum-assistant/` +2. Import `VellumAssistantShared` for access to IPC types +3. Can use AppKit, ScreenCaptureKit, etc. freely + +### Adding iOS Code (Future) +1. Place in `clients/ios/vellum-assistant-ios/` +2. Import `VellumAssistantShared` for IPC, design tokens, ViewModels +3. DO NOT import `VellumAssistantLib` (macOS-only) + +## Known Limitations + +### iOS TCP Connection +- Currently plaintext (no TLS) +- Safe for localhost development only +- TLS layer tracked for PR 11 (Daemon authentication) + +### iOS Signing Operations +- iOS clients log errors when daemon sends signing requests +- Cannot send error responses (protocol limitation) +- Daemon should detect iOS clients and avoid sending these messages + +### iOS Localhost Default +- iOS defaults to `localhost:8765` in UserDefaults +- Real device usage requires configuring daemon hostname +- PR 6 (iOS settings/onboarding) will provide UI for configuration + +## Documentation + +- **macOS development**: See `clients/macos/CLAUDE.md` +- **PR #1821**: [iOS shared library foundation](https://github.com/vellum-ai/vellum-assistant/pull/1821) +- **iOS rollout plan**: See `.private/plans/sharded-mapping-shannon.md` (13 PRs) + +## Testing + +```bash +cd clients/macos +./build.sh test # All SPM tests (both shared and macOS-specific) +``` + +Tests use mock implementations of protocols for dependency injection: +- `DaemonClientProtocol` → `MockDaemonClient` +- `AccessibilityTreeProviding` → `MockAccessibilityTree` +- `ScreenCaptureProviding` → `MockScreenCapture` diff --git a/clients/macos/build.sh b/clients/macos/build.sh index 670bc67f27e..f2224eb5daa 100755 --- a/clients/macos/build.sh +++ b/clients/macos/build.sh @@ -61,7 +61,7 @@ case "$CMD" in ;; clean) echo "Cleaning..." - rm -rf "$SCRIPT_DIR/dist" "$SCRIPT_DIR/.build" + rm -rf "$SCRIPT_DIR/dist" "$SCRIPT_DIR/../.build" echo "Done." exit 0 ;; @@ -80,7 +80,7 @@ if [ "$CMD" = "release" ]; then SWIFT_FLAGS="-c release" # Force clean for release builds to prevent stale artifacts in production echo "Release build: forcing clean to ensure no stale artifacts..." - rm -rf "$SCRIPT_DIR/dist" "$SCRIPT_DIR/.build" + rm -rf "$SCRIPT_DIR/dist" "$SCRIPT_DIR/../.build" fi # 1. Build with SPM diff --git a/clients/macos/vellum-assistant/Ambient/AmbientAgent.swift b/clients/macos/vellum-assistant/Ambient/AmbientAgent.swift index 117f6939f34..0e8405127c9 100644 --- a/clients/macos/vellum-assistant/Ambient/AmbientAgent.swift +++ b/clients/macos/vellum-assistant/Ambient/AmbientAgent.swift @@ -1,4 +1,5 @@ import Foundation +import VellumAssistantShared import AppKit import Combine import UserNotifications diff --git a/clients/macos/vellum-assistant/App/AppDelegate.swift b/clients/macos/vellum-assistant/App/AppDelegate.swift index 4b859bad3e1..fdc4a5c1013 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate.swift @@ -1,4 +1,5 @@ import AppKit +import VellumAssistantShared import Combine import CoreText import HotKey diff --git a/clients/macos/vellum-assistant/ComputerUse/RecipeExecutor.swift b/clients/macos/vellum-assistant/ComputerUse/RecipeExecutor.swift index 1e1ca40f654..bd92a8ffbc5 100644 --- a/clients/macos/vellum-assistant/ComputerUse/RecipeExecutor.swift +++ b/clients/macos/vellum-assistant/ComputerUse/RecipeExecutor.swift @@ -1,4 +1,5 @@ import Foundation +import VellumAssistantShared import os private let log = Logger( diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 7ccef9fb3b4..d1c8ed5ffce 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -1,4 +1,5 @@ import Foundation +import VellumAssistantShared import CoreGraphics import AppKit import os diff --git a/clients/macos/vellum-assistant/ComputerUse/TextSession.swift b/clients/macos/vellum-assistant/ComputerUse/TextSession.swift index 0ee9d38a0af..bb33c73cbd2 100644 --- a/clients/macos/vellum-assistant/ComputerUse/TextSession.swift +++ b/clients/macos/vellum-assistant/ComputerUse/TextSession.swift @@ -1,4 +1,5 @@ import Foundation +import VellumAssistantShared import os private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant", category: "TextSession") diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatMessage.swift b/clients/macos/vellum-assistant/Features/Chat/ChatMessage.swift index 59d977dfdb1..96e54af3ab1 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatMessage.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatMessage.swift @@ -1,5 +1,6 @@ import AppKit import Foundation +import VellumAssistantShared enum ChatRole: String { case user diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatView.swift b/clients/macos/vellum-assistant/Features/Chat/ChatView.swift index 9aef4480815..849a221771f 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatView.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatView.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared import UniformTypeIdentifiers struct ChatView: View { diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatViewModel.swift b/clients/macos/vellum-assistant/Features/Chat/ChatViewModel.swift index 7509b07ba7e..ce8e805f252 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatViewModel.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatViewModel.swift @@ -1,4 +1,5 @@ import Foundation +import VellumAssistantShared import os import UniformTypeIdentifiers import AppKit diff --git a/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineCardWidget.swift b/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineCardWidget.swift index e5daafdb217..86e95d40a84 100644 --- a/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineCardWidget.swift +++ b/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineCardWidget.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared /// Inline card widget for displaying structured information in chat. /// Supports template-based rendering for specialized layouts (e.g. weather forecasts). diff --git a/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineFallbackChip.swift b/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineFallbackChip.swift index e8a240e958f..a12f5b35569 100644 --- a/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineFallbackChip.swift +++ b/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineFallbackChip.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared /// Fallback view for unsupported inline surface types. struct InlineFallbackChip: View { diff --git a/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineListWidget.swift b/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineListWidget.swift index 34510d895c8..4fef3b27af9 100644 --- a/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineListWidget.swift +++ b/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineListWidget.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared /// Inline list widget for selectable items in chat. struct InlineListWidget: View { diff --git a/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineSurfaceRouter.swift b/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineSurfaceRouter.swift index 7c7d49c4dc9..6e7efb9f9b8 100644 --- a/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineSurfaceRouter.swift +++ b/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineSurfaceRouter.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared /// Routes an `InlineSurfaceData` to the correct inline widget view. struct InlineSurfaceRouter: View { diff --git a/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineTableWidget.swift b/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineTableWidget.swift index 77b90d534a7..45447e9c4b9 100644 --- a/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineTableWidget.swift +++ b/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineTableWidget.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared /// Inline table widget with selectable rows and action support. struct InlineTableWidget: View { diff --git a/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineWeatherWidget.swift b/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineWeatherWidget.swift index 9fb9ffc0aea..075fab47a3f 100644 --- a/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineWeatherWidget.swift +++ b/clients/macos/vellum-assistant/Features/Chat/InlineWidgets/InlineWeatherWidget.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared // MARK: - Data Model diff --git a/clients/macos/vellum-assistant/Features/Chat/ToolCallChip.swift b/clients/macos/vellum-assistant/Features/Chat/ToolCallChip.swift index 8cd6e422e5a..8a2e6825728 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ToolCallChip.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ToolCallChip.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared struct ToolCallChip: View { let toolCall: ToolCallData diff --git a/clients/macos/vellum-assistant/Features/Chat/ToolConfirmationBubble.swift b/clients/macos/vellum-assistant/Features/Chat/ToolConfirmationBubble.swift index 4403d4163c5..e23c7d14dd8 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ToolConfirmationBubble.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ToolConfirmationBubble.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared struct ToolConfirmationBubble: View { let confirmation: ToolConfirmationData @@ -274,12 +275,12 @@ struct ToolConfirmationBubble: View { riskLevel: "medium", diff: nil, allowlistOptions: [ - .init(label: "git push", description: "This exact command", pattern: "git push"), - .init(label: "git *", description: "Any git command", pattern: "git *"), + ConfirmationRequestMessage.ConfirmationAllowlistOption(label: "git push", description: "This exact command", pattern: "git push"), + ConfirmationRequestMessage.ConfirmationAllowlistOption(label: "git *", description: "Any git command", pattern: "git *"), ], scopeOptions: [ - .init(label: "This project", scope: "/Users/test/project"), - .init(label: "Everywhere", scope: "everywhere"), + ConfirmationRequestMessage.ConfirmationScopeOption(label: "This project", scope: "/Users/test/project"), + ConfirmationRequestMessage.ConfirmationScopeOption(label: "Everywhere", scope: "everywhere"), ] ), onAllow: {}, @@ -311,10 +312,10 @@ struct ToolConfirmationBubble: View { riskLevel: "medium", diff: nil, allowlistOptions: [ - .init(label: "npm install", description: "This exact command", pattern: "npm install"), + ConfirmationRequestMessage.ConfirmationAllowlistOption(label: "npm install", description: "This exact command", pattern: "npm install"), ], scopeOptions: [ - .init(label: "Everywhere", scope: "everywhere"), + ConfirmationRequestMessage.ConfirmationScopeOption(label: "Everywhere", scope: "everywhere"), ], state: .approved ), diff --git a/clients/macos/vellum-assistant/Features/MainWindow/MainWindow.swift b/clients/macos/vellum-assistant/Features/MainWindow/MainWindow.swift index a4cf5dfe4d0..ef7ec35738d 100644 --- a/clients/macos/vellum-assistant/Features/MainWindow/MainWindow.swift +++ b/clients/macos/vellum-assistant/Features/MainWindow/MainWindow.swift @@ -1,4 +1,5 @@ import AppKit +import VellumAssistantShared import SwiftUI @MainActor diff --git a/clients/macos/vellum-assistant/Features/MainWindow/MainWindowView.swift b/clients/macos/vellum-assistant/Features/MainWindow/MainWindowView.swift index 1a41a446247..c6976cdd2b9 100644 --- a/clients/macos/vellum-assistant/Features/MainWindow/MainWindowView.swift +++ b/clients/macos/vellum-assistant/Features/MainWindow/MainWindowView.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared import UniformTypeIdentifiers struct MainWindowView: View { diff --git a/clients/macos/vellum-assistant/Features/MainWindow/Panels/AgentPanel.swift b/clients/macos/vellum-assistant/Features/MainWindow/Panels/AgentPanel.swift index fddb6fa28e5..94d5c35ff3f 100644 --- a/clients/macos/vellum-assistant/Features/MainWindow/Panels/AgentPanel.swift +++ b/clients/macos/vellum-assistant/Features/MainWindow/Panels/AgentPanel.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared // MARK: - Agent Panel diff --git a/clients/macos/vellum-assistant/Features/MainWindow/Panels/GeneratedPanel.swift b/clients/macos/vellum-assistant/Features/MainWindow/Panels/GeneratedPanel.swift index e58167de534..9435cd71e98 100644 --- a/clients/macos/vellum-assistant/Features/MainWindow/Panels/GeneratedPanel.swift +++ b/clients/macos/vellum-assistant/Features/MainWindow/Panels/GeneratedPanel.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared /// Unified display item for both local and shared apps. private struct DisplayAppItem: Identifiable { diff --git a/clients/macos/vellum-assistant/Features/MainWindow/Panels/SettingsPanel.swift b/clients/macos/vellum-assistant/Features/MainWindow/Panels/SettingsPanel.swift index 4f2e9ebe2c0..8fd67137460 100644 --- a/clients/macos/vellum-assistant/Features/MainWindow/Panels/SettingsPanel.swift +++ b/clients/macos/vellum-assistant/Features/MainWindow/Panels/SettingsPanel.swift @@ -1,5 +1,6 @@ import Combine import SwiftUI +import VellumAssistantShared @MainActor struct SettingsPanel: View { diff --git a/clients/macos/vellum-assistant/Features/MainWindow/Panels/SkillsManager.swift b/clients/macos/vellum-assistant/Features/MainWindow/Panels/SkillsManager.swift index 7539ecdc9ba..f675717f30b 100644 --- a/clients/macos/vellum-assistant/Features/MainWindow/Panels/SkillsManager.swift +++ b/clients/macos/vellum-assistant/Features/MainWindow/Panels/SkillsManager.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared @MainActor final class SkillsManager: ObservableObject { diff --git a/clients/macos/vellum-assistant/Features/MainWindow/ThreadManager.swift b/clients/macos/vellum-assistant/Features/MainWindow/ThreadManager.swift index 0857ef190a2..d3df74b21a9 100644 --- a/clients/macos/vellum-assistant/Features/MainWindow/ThreadManager.swift +++ b/clients/macos/vellum-assistant/Features/MainWindow/ThreadManager.swift @@ -1,4 +1,5 @@ import Combine +import VellumAssistantShared import Foundation import os diff --git a/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingFlowView.swift b/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingFlowView.swift index f59c07a0181..56fc2befd1e 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingFlowView.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingFlowView.swift @@ -1,4 +1,5 @@ import SpriteKit +import VellumAssistantShared import SwiftUI @MainActor diff --git a/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingIntroductionView.swift b/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingIntroductionView.swift index f85fd346508..1ec631c9b56 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingIntroductionView.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingIntroductionView.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared @MainActor struct FirstMeetingIntroductionView: View { diff --git a/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingIntroductionViewModel.swift b/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingIntroductionViewModel.swift index 902a5e6441e..2cf4b8e091b 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingIntroductionViewModel.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingIntroductionViewModel.swift @@ -1,4 +1,5 @@ import Foundation +import VellumAssistantShared import Observation import os diff --git a/clients/macos/vellum-assistant/Features/Onboarding/Interview/InterviewStepView.swift b/clients/macos/vellum-assistant/Features/Onboarding/Interview/InterviewStepView.swift index b9ef63d0e00..c9c3acd20f9 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/Interview/InterviewStepView.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/Interview/InterviewStepView.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared @MainActor struct InterviewStepView: View { diff --git a/clients/macos/vellum-assistant/Features/Onboarding/Interview/InterviewViewModel.swift b/clients/macos/vellum-assistant/Features/Onboarding/Interview/InterviewViewModel.swift index 126cb1013de..9aee0f1503f 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/Interview/InterviewViewModel.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/Interview/InterviewViewModel.swift @@ -1,4 +1,5 @@ import Foundation +import VellumAssistantShared import Observation import os diff --git a/clients/macos/vellum-assistant/Features/Onboarding/Interview/ProfileExtractor.swift b/clients/macos/vellum-assistant/Features/Onboarding/Interview/ProfileExtractor.swift index b015d0acc9b..0ad79bf3212 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/Interview/ProfileExtractor.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/Interview/ProfileExtractor.swift @@ -1,4 +1,5 @@ import Foundation +import VellumAssistantShared import Observation import os diff --git a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingFlowView.swift b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingFlowView.swift index f9af5519ad6..d78457534e1 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingFlowView.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingFlowView.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared @MainActor struct OnboardingFlowView: View { diff --git a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingWindow.swift b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingWindow.swift index e3a9a5b5fc9..cce2a42752f 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/OnboardingWindow.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/OnboardingWindow.swift @@ -1,4 +1,5 @@ import AppKit +import VellumAssistantShared import SwiftUI @MainActor diff --git a/clients/macos/vellum-assistant/Features/Settings/SettingsView.swift b/clients/macos/vellum-assistant/Features/Settings/SettingsView.swift index 36c485e924a..5b1050d03d8 100644 --- a/clients/macos/vellum-assistant/Features/Settings/SettingsView.swift +++ b/clients/macos/vellum-assistant/Features/Settings/SettingsView.swift @@ -1,5 +1,6 @@ import Combine import SwiftUI +import VellumAssistantShared public struct SettingsView: View { @State private var apiKeyText = "" diff --git a/clients/macos/vellum-assistant/Features/Settings/SkillsSettingsView.swift b/clients/macos/vellum-assistant/Features/Settings/SkillsSettingsView.swift index 1e98fe50906..3c94c1823d3 100644 --- a/clients/macos/vellum-assistant/Features/Settings/SkillsSettingsView.swift +++ b/clients/macos/vellum-assistant/Features/Settings/SkillsSettingsView.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared // MARK: - View Model diff --git a/clients/macos/vellum-assistant/Features/Settings/TrustRulesView.swift b/clients/macos/vellum-assistant/Features/Settings/TrustRulesView.swift index 6a9c453c406..cc9d4316b1a 100644 --- a/clients/macos/vellum-assistant/Features/Settings/TrustRulesView.swift +++ b/clients/macos/vellum-assistant/Features/Settings/TrustRulesView.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared // MARK: - Trust Rules View diff --git a/clients/macos/vellum-assistant/Features/Sharing/BundleConfirmationViewModel.swift b/clients/macos/vellum-assistant/Features/Sharing/BundleConfirmationViewModel.swift index e40be1b07de..79b10fe6cbf 100644 --- a/clients/macos/vellum-assistant/Features/Sharing/BundleConfirmationViewModel.swift +++ b/clients/macos/vellum-assistant/Features/Sharing/BundleConfirmationViewModel.swift @@ -1,5 +1,6 @@ import Foundation import SwiftUI +import VellumAssistantShared @MainActor final class BundleConfirmationViewModel: ObservableObject { diff --git a/clients/macos/vellum-assistant/Features/Sharing/BundleSandbox.swift b/clients/macos/vellum-assistant/Features/Sharing/BundleSandbox.swift index ddd6ee70674..388a3f3ff1c 100644 --- a/clients/macos/vellum-assistant/Features/Sharing/BundleSandbox.swift +++ b/clients/macos/vellum-assistant/Features/Sharing/BundleSandbox.swift @@ -1,5 +1,6 @@ import Foundation import os +import VellumAssistantShared private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant", category: "BundleSandbox") diff --git a/clients/macos/vellum-assistant/Features/Surfaces/DynamicPageSurfaceView.swift b/clients/macos/vellum-assistant/Features/Surfaces/DynamicPageSurfaceView.swift index 8b14613f67a..e823c519334 100644 --- a/clients/macos/vellum-assistant/Features/Surfaces/DynamicPageSurfaceView.swift +++ b/clients/macos/vellum-assistant/Features/Surfaces/DynamicPageSurfaceView.swift @@ -1,6 +1,7 @@ import SwiftUI @preconcurrency import WebKit import os +import VellumAssistantShared private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant", category: "DynamicPage") diff --git a/clients/macos/vellum-assistant/Features/Surfaces/SecretPromptManager.swift b/clients/macos/vellum-assistant/Features/Surfaces/SecretPromptManager.swift index 9f2d6d53145..ea622c4aba7 100644 --- a/clients/macos/vellum-assistant/Features/Surfaces/SecretPromptManager.swift +++ b/clients/macos/vellum-assistant/Features/Surfaces/SecretPromptManager.swift @@ -1,6 +1,7 @@ import AppKit import SwiftUI import os +import VellumAssistantShared private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant", category: "SecretPromptManager") diff --git a/clients/macos/vellum-assistant/Features/Surfaces/SurfaceManager.swift b/clients/macos/vellum-assistant/Features/Surfaces/SurfaceManager.swift index 20d74203bab..701cd66cd2d 100644 --- a/clients/macos/vellum-assistant/Features/Surfaces/SurfaceManager.swift +++ b/clients/macos/vellum-assistant/Features/Surfaces/SurfaceManager.swift @@ -1,6 +1,7 @@ import AppKit import SwiftUI import os +import VellumAssistantShared private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant", category: "SurfaceManager") diff --git a/clients/macos/vellum-assistant/Features/Surfaces/SurfaceTypes.swift b/clients/macos/vellum-assistant/Features/Surfaces/SurfaceTypes.swift index e835c66accc..87854dbed78 100644 --- a/clients/macos/vellum-assistant/Features/Surfaces/SurfaceTypes.swift +++ b/clients/macos/vellum-assistant/Features/Surfaces/SurfaceTypes.swift @@ -1,4 +1,5 @@ import Foundation +import VellumAssistantShared // MARK: - Surface Enums diff --git a/clients/macos/vellum-assistant/Features/Surfaces/ToolConfirmationManager.swift b/clients/macos/vellum-assistant/Features/Surfaces/ToolConfirmationManager.swift index 3dba83111b9..4a2ccd657d2 100644 --- a/clients/macos/vellum-assistant/Features/Surfaces/ToolConfirmationManager.swift +++ b/clients/macos/vellum-assistant/Features/Surfaces/ToolConfirmationManager.swift @@ -1,6 +1,7 @@ import AppKit import SwiftUI import os +import VellumAssistantShared private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.vellum.vellum-assistant", category: "ToolConfirmationManager") diff --git a/clients/macos/vellum-assistant/Features/TaskInput/TaskInputView.swift b/clients/macos/vellum-assistant/Features/TaskInput/TaskInputView.swift index f73c01ab383..f50ffd91618 100644 --- a/clients/macos/vellum-assistant/Features/TaskInput/TaskInputView.swift +++ b/clients/macos/vellum-assistant/Features/TaskInput/TaskInputView.swift @@ -1,4 +1,5 @@ import SwiftUI +import VellumAssistantShared import AppKit import UniformTypeIdentifiers diff --git a/clients/macos/vellum-assistant/IPC/IPCMessages.swift b/clients/macos/vellum-assistant/IPC/IPCMessages.swift deleted file mode 100644 index 689eaa7c87a..00000000000 --- a/clients/macos/vellum-assistant/IPC/IPCMessages.swift +++ /dev/null @@ -1,1149 +0,0 @@ -import Foundation - -// MARK: - AnyCodable - -/// Lightweight wrapper for arbitrary JSON values in tool input dictionaries. -/// Supports String, Int, Double, Bool, null, arrays, and nested objects. -struct AnyCodable: Codable, Equatable, @unchecked Sendable { - let value: Any? - - init(_ value: Any?) { - self.value = value - } - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if container.decodeNil() { - value = nil - } else if let bool = try? container.decode(Bool.self) { - value = bool - } else if let int = try? container.decode(Int.self) { - value = int - } else if let double = try? container.decode(Double.self) { - value = double - } else if let string = try? container.decode(String.self) { - value = string - } else if let array = try? container.decode([AnyCodable].self) { - value = array.map { $0.value } - } else if let dict = try? container.decode([String: AnyCodable].self) { - value = dict.mapValues { $0.value } - } else { - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON value type") - } - } - - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - if value == nil { - try container.encodeNil() - } else if let bool = value as? Bool { - try container.encode(bool) - } else if let int = value as? Int { - try container.encode(int) - } else if let double = value as? Double { - try container.encode(double) - } else if let string = value as? String { - try container.encode(string) - } else if let array = value as? [Any?] { - try container.encode(array.map { AnyCodable($0) }) - } else if let dict = value as? [String: Any?] { - try container.encode(dict.mapValues { AnyCodable($0) }) - } else { - try container.encodeNil() - } - } - - static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool { - switch (lhs.value, rhs.value) { - case (nil, nil): - return true - case let (l as Bool, r as Bool): - return l == r - case let (l as Int, r as Int): - return l == r - case let (l as Double, r as Double): - return l == r - case let (l as String, r as String): - return l == r - case let (l as [Any?], r as [Any?]): - return l.count == r.count && zip(l, r).allSatisfy { AnyCodable($0) == AnyCodable($1) } - case let (l as [String: Any?], r as [String: Any?]): - guard l.count == r.count else { return false } - return l.allSatisfy { key, lVal in - guard let rVal = r[key] else { return false } - return AnyCodable(lVal) == AnyCodable(rVal) - } - default: - return false - } - } -} - -// MARK: - Client → Server Messages (Encodable) - -/// Attachment payload sent inline as base64. Mirrors `UserMessageAttachment` from ipc-protocol.ts. -struct IPCAttachment: Codable, Sendable { - let filename: String - let mimeType: String - let data: String - let extractedText: String? -} - -/// Sent to create a new computer-use session. -/// Wire type: `"cu_session_create"` -struct CuSessionCreateMessage: Encodable, Sendable { - let type: String = "cu_session_create" - let sessionId: String - let task: String - let screenWidth: Int - let screenHeight: Int - let attachments: [IPCAttachment]? - let interactionType: String? -} - -/// Sent after each perceive step with AX tree, screenshot, and execution results. -/// Wire type: `"cu_observation"` -struct CuObservationMessage: Encodable, Sendable { - let type: String = "cu_observation" - let sessionId: String - let axTree: String? - let axDiff: String? - let secondaryWindows: String? - let screenshot: String? - let executionResult: String? - let executionError: String? -} - -/// Sent by the ambient agent with OCR text from periodic screen captures. -/// Wire type: `"ambient_observation"` -struct AmbientObservationMessage: Encodable, Sendable { - let type: String = "ambient_observation" - let requestId: String - let ocrText: String - let appName: String? - let windowTitle: String? - let timestamp: Double -} - -/// Sent to create a new Q&A session. -/// Wire type: `"session_create"` -struct SessionCreateMessage: Encodable, Sendable { - let type: String = "session_create" - let title: String? - let systemPromptOverride: String? - let maxResponseTokens: Int? - /// Client-generated nonce echoed back in `session_info` so the caller can - /// correlate the response to its specific request. Prevents multiple - /// ChatViewModels sharing one DaemonClient from stealing each other's sessions. - let correlationId: String? - - init(title: String?, systemPromptOverride: String? = nil, maxResponseTokens: Int? = nil, correlationId: String? = nil) { - self.title = title - self.systemPromptOverride = systemPromptOverride - self.maxResponseTokens = maxResponseTokens - self.correlationId = correlationId - } -} - -/// Sent to add a user message to an existing Q&A session. -/// Wire type: `"user_message"` -struct UserMessageMessage: Encodable, Sendable { - let type: String = "user_message" - let sessionId: String - let content: String - let attachments: [IPCAttachment]? -} - -/// Sent to request daemon-side classification and session creation. -/// Wire type: `"task_submit"` -struct TaskSubmitMessage: Encodable, Sendable { - let type: String = "task_submit" - let task: String - let screenWidth: Int - let screenHeight: Int - let attachments: [IPCAttachment]? - let source: String? -} - -/// Sent to cancel the active generation. -/// Wire type: `"cancel"` -struct CancelMessage: Encodable, Sendable { - let type: String = "cancel" - let sessionId: String -} - -/// Sent to abort a running computer-use session. -/// Wire type: `"cu_session_abort"` -struct CuSessionAbortMessage: Encodable, Sendable { - let type: String = "cu_session_abort" - let sessionId: String -} - -/// Keepalive ping. -/// Wire type: `"ping"` -struct PingMessage: Encodable, Sendable { - let type: String = "ping" -} - -/// Sent when user interacts with a surface. -/// Wire type: `"ui_surface_action"` -struct UiSurfaceActionMessage: Encodable, Sendable { - let type: String = "ui_surface_action" - let sessionId: String - let surfaceId: String - let actionId: String - let data: [String: AnyCodable]? -} - -/// Sent when a persistent app's JS makes a data request via the RPC bridge. -/// Wire type: `"app_data_request"` -struct AppDataRequestMessage: Encodable, Sendable { - let type: String = "app_data_request" - let surfaceId: String - let callId: String - let method: String - let appId: String - let recordId: String? - let data: [String: AnyCodable]? -} - -/// Sent to request the list of all apps. -/// Wire type: `"apps_list"` -struct AppsListRequestMessage: Encodable, Sendable { - let type: String = "apps_list" -} - -/// Sent to request the list of shared/received apps. -/// Wire type: `"shared_apps_list"` -struct SharedAppsListRequestMessage: Encodable, Sendable { - let type: String = "shared_apps_list" -} - -/// Sent to delete a shared app by UUID. -/// Wire type: `"shared_app_delete"` -struct SharedAppDeleteRequestMessage: Encodable, Sendable { - let type: String = "shared_app_delete" - let uuid: String -} - -/// Sent to request bundling an app for sharing. -/// Wire type: `"bundle_app"` -struct BundleAppRequestMessage: Encodable, Sendable { - let type: String = "bundle_app" - let appId: String -} - -/// Sent to open and scan a .vellumapp bundle. -/// Wire type: `"open_bundle"` -struct OpenBundleMessage: Encodable, Sendable { - let type: String = "open_bundle" - let filePath: String -} - -/// Sent to request the list of all past sessions/conversations. -/// Wire type: `"session_list"` -struct SessionListRequestMessage: Encodable, Sendable { - let type: String = "session_list" -} - -/// Sent to request message history for a specific session. -/// Wire type: `"history_request"` -struct HistoryRequestMessage: Encodable, Sendable { - let type: String = "history_request" - let sessionId: String -} - -/// Sent to request the list of available skills. -/// Wire type: `"skills_list"` -struct SkillsListRequestMessage: Encodable, Sendable { - let type: String = "skills_list" -} - -/// Sent to request the full body of a specific skill. -/// Wire type: `"skill_detail"` -struct SkillDetailRequestMessage: Encodable, Sendable { - let type: String = "skill_detail" - let skillId: String -} - -/// Enable a skill. Wire type: "skills_enable" -struct SkillsEnableMessage: Encodable, Sendable { - let type: String = "skills_enable" - let name: String -} - -/// Disable a skill. Wire type: "skills_disable" -struct SkillsDisableMessage: Encodable, Sendable { - let type: String = "skills_disable" - let name: String -} - -/// Configure a skill's env/apiKey/config. Wire type: "skills_configure" -struct SkillsConfigureMessage: Encodable, Sendable { - let type: String = "skills_configure" - let name: String - let env: [String: String]? - let apiKey: String? - let config: [String: AnyCodable]? -} - -/// Install a skill from ClaWHub. Wire type: "skills_install" -struct SkillsInstallMessage: Encodable, Sendable { - let type: String = "skills_install" - let slug: String - let version: String? -} - -/// Uninstall a skill. Wire type: "skills_uninstall" -struct SkillsUninstallMessage: Encodable, Sendable { - let type: String = "skills_uninstall" - let name: String -} - -/// Update a skill. Wire type: "skills_update" -struct SkillsUpdateMessage: Encodable, Sendable { - let type: String = "skills_update" - let name: String -} - -/// Check for skill updates. Wire type: "skills_check_updates" -struct SkillsCheckUpdatesMessage: Encodable, Sendable { - let type: String = "skills_check_updates" -} - -/// Search for skills on ClaWHub. Wire type: "skills_search" -struct SkillsSearchMessage: Encodable, Sendable { - let type: String = "skills_search" - let query: String -} - -/// Inspect a ClaWHub skill for detailed info. Wire type: "skills_inspect" -struct SkillsInspectMessage: Encodable, Sendable { - let type: String = "skills_inspect" - let slug: String -} - -/// Response to a sign_bundle_payload request from the daemon. -/// Wire type: `"sign_bundle_payload_response"` -struct SignBundlePayloadResponseMessage: Encodable, Sendable { - let type: String = "sign_bundle_payload_response" - let signature: String - let keyId: String - let publicKey: String -} - -/// Response to a get_signing_identity request from the daemon. -/// Wire type: `"get_signing_identity_response"` -struct GetSigningIdentityResponseMessage: Encodable, Sendable { - let type: String = "get_signing_identity_response" - let keyId: String - let publicKey: String -} - -// MARK: - Server → Client Messages (Decodable) - -/// Action to execute from the inference server. -struct CuActionMessage: Decodable, Sendable { - let sessionId: String - let toolName: String - let input: [String: AnyCodable] - let reasoning: String? - let stepNumber: Int -} - -/// Session completed successfully. -struct CuCompleteMessage: Decodable, Sendable { - let sessionId: String - let summary: String - let stepCount: Int - let isResponse: Bool? -} - -/// Session-level error from the server. -struct CuErrorMessage: Decodable, Sendable { - let sessionId: String - let message: String -} - -/// Streamed text delta from the assistant's response. -struct AssistantTextDeltaMessage: Decodable, Sendable { - let text: String - let sessionId: String? - - init(text: String, sessionId: String? = nil) { - self.text = text - self.sessionId = sessionId - } -} - -/// Streamed thinking delta from the assistant's reasoning. -struct AssistantThinkingDeltaMessage: Decodable, Sendable { - let thinking: String -} - -/// Signals that the assistant's message is complete. -struct MessageCompleteMessage: Decodable, Sendable { - let sessionId: String? - - init(sessionId: String? = nil) { - self.sessionId = sessionId - } -} - -/// Session metadata from the server (e.g. generated title). -struct SessionInfoMessage: Decodable, Sendable { - let sessionId: String - let title: String - /// Echoed from the `session_create` request so the caller can match - /// this response to its specific request. - let correlationId: String? - - init(sessionId: String, title: String, correlationId: String? = nil) { - self.sessionId = sessionId - self.title = title - self.correlationId = correlationId - } -} - -/// Daemon response after classifying and routing a task_submit. -struct TaskRoutedMessage: Decodable, Sendable { - let sessionId: String - let interactionType: String - /// The task text passed to the escalated session. - let task: String? - /// Set when a text_qa session escalates to computer_use via request_computer_control. - let escalatedFrom: String? -} - -/// Result from ambient observation analysis. -struct AmbientResultMessage: Decodable, Sendable { - let requestId: String - let decision: String - let summary: String? - let suggestion: String? -} - -/// Surface show command from daemon. -/// Wire type: `"ui_surface_show"` -struct UiSurfaceShowMessage: Decodable, Sendable { - let sessionId: String - let surfaceId: String - let surfaceType: String - let title: String? - let data: AnyCodable - let actions: [SurfaceActionData]? - /// `"inline"` embeds in chat, `"panel"` shows a floating window. - let display: String? -} - -struct SurfaceActionData: Decodable, Sendable { - let id: String - let label: String - let style: String? -} - -/// Surface update command from daemon. -/// Wire type: `"ui_surface_update"` -struct UiSurfaceUpdateMessage: Decodable, Sendable { - let sessionId: String - let surfaceId: String - let data: AnyCodable -} - -/// Surface dismiss command from daemon. -/// Wire type: `"ui_surface_dismiss"` -struct UiSurfaceDismissMessage: Decodable, Sendable { - let sessionId: String - let surfaceId: String -} - -/// Confirms generation was cancelled. -struct GenerationCancelledMessage: Decodable, Sendable { - let sessionId: String? -} - -/// Notifies client that active generation yielded to queued work at a checkpoint. -/// Wire type: `"generation_handoff"` -struct GenerationHandoffMessage: Decodable, Sendable { - let sessionId: String - let requestId: String? - let queuedCount: Int -} - -/// Notifies client that a message has been queued for processing. -/// Wire type: `"message_queued"` -struct MessageQueuedMessage: Decodable, Sendable { - let sessionId: String - let requestId: String - let position: Int -} - -/// Notifies client that a queued message has been dequeued and is now being processed. -/// Wire type: `"message_dequeued"` -struct MessageDequeuedMessage: Decodable, Sendable { - let sessionId: String - let requestId: String -} - -/// Server-level error message. -struct ErrorMessage: Decodable, Sendable { - let message: String -} - -/// Response from the daemon for a persistent app data request. -/// Wire type: `"app_data_response"` -struct AppDataResponseMessage: Decodable, Sendable { - let surfaceId: String - let callId: String - let success: Bool - let result: AnyCodable? - let error: String? -} - -/// ClaWHub metadata for a skill. -struct ClawhubInfo: Codable, Sendable { - let author: String - let stars: Int - let installs: Int - let reports: Int - let publishedAt: String -} - -/// Missing requirements preventing a skill from full operation. -struct MissingRequirements: Codable, Sendable { - let bins: [String]? - let env: [String]? - let permissions: [String]? -} - -/// Full skill info from the daemon's resolved skill list. -struct SkillInfo: Codable, Sendable, Identifiable { - var id: String { name } - let name: String - let description: String - let emoji: String? - let homepage: String? - let source: String // "bundled" | "managed" | "workspace" | "clawhub" | "extra" - let state: String // "enabled" | "disabled" | "available" - let degraded: Bool - let missingRequirements: MissingRequirements? - let installedVersion: String? - let latestVersion: String? - let updateAvailable: Bool - let userInvocable: Bool - let clawhub: ClawhubInfo? -} - -/// Backward-compatible alias for code referencing the old name. -typealias SkillSummaryItem = SkillInfo - -/// Response containing the list of available skills. -/// Wire type: `"skills_list_response"` -struct SkillsListResponseMessage: Decodable, Sendable { - let skills: [SkillInfo] -} - -/// Response containing the full body of a specific skill. -/// Wire type: `"skill_detail_response"` -struct SkillDetailResponseMessage: Decodable, Sendable { - let skillId: String - let body: String - let error: String? -} - -/// Push event: skill state changed. Wire type: "skills_state_changed" -struct SkillStateChangedMessage: Decodable, Sendable { - let name: String - let state: String // "enabled" | "disabled" | "installed" | "uninstalled" -} - -/// Push event: updates available. Wire type: "skills_updates_available" -struct SkillsUpdatesAvailableMessage: Decodable, Sendable { - struct UpdateInfo: Decodable, Sendable { - let name: String - let installedVersion: String - let latestVersion: String - } - let skills: [UpdateInfo] -} - -/// A ClaWHub skill returned from a search or explore query. -struct ClawhubSkillItem: Decodable, Sendable, Identifiable, Equatable { - var id: String { slug } - let name: String - let slug: String - let description: String - let author: String - let stars: Int - let installs: Int - let version: String - /// Epoch milliseconds when the skill was first published. - let createdAt: Int - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - name = try container.decodeIfPresent(String.self, forKey: .name) ?? "" - slug = try container.decode(String.self, forKey: .slug) - description = try container.decodeIfPresent(String.self, forKey: .description) ?? "" - author = try container.decodeIfPresent(String.self, forKey: .author) ?? "" - stars = try container.decodeIfPresent(Int.self, forKey: .stars) ?? 0 - installs = try container.decodeIfPresent(Int.self, forKey: .installs) ?? 0 - version = try container.decodeIfPresent(String.self, forKey: .version) ?? "" - createdAt = try container.decodeIfPresent(Int.self, forKey: .createdAt) ?? 0 - } - - private enum CodingKeys: String, CodingKey { - case name, slug, description, author, stars, installs, version, createdAt - } -} - -/// Wrapper for ClaWHub search results embedded in `skills_operation_response.data`. -struct ClawhubSearchData: Decodable, Sendable { - let skills: [ClawhubSkillItem] -} - -/// Generic operation response. Wire type: "skills_operation_response" -struct SkillsOperationResponseMessage: Decodable, Sendable { - let operation: String - let success: Bool - let error: String? - let data: ClawhubSearchData? -} - -/// Skill info from a ClaWHub inspect response. -struct ClawhubInspectSkill: Decodable, Sendable { - let slug: String - let displayName: String - let summary: String -} - -/// Owner info from a ClaWHub inspect response. -struct ClawhubInspectOwner: Decodable, Sendable { - let handle: String - let displayName: String - let image: String? -} - -/// Stats from a ClaWHub inspect response. -struct ClawhubInspectStats: Decodable, Sendable { - let stars: Int - let installs: Int - let downloads: Int - let versions: Int - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - stars = try container.decodeIfPresent(Int.self, forKey: .stars) ?? 0 - installs = try container.decodeIfPresent(Int.self, forKey: .installs) ?? 0 - downloads = try container.decodeIfPresent(Int.self, forKey: .downloads) ?? 0 - versions = try container.decodeIfPresent(Int.self, forKey: .versions) ?? 0 - } - - private enum CodingKeys: String, CodingKey { - case stars, installs, downloads, versions - } -} - -/// Version info from a ClaWHub inspect response. -struct ClawhubInspectVersion: Decodable, Sendable { - let version: String - let changelog: String? -} - -/// File entry from a ClaWHub inspect response. -struct ClawhubInspectFile: Decodable, Sendable { - let path: String - let size: Int - let contentType: String? -} - -/// Full inspect data for a ClaWHub skill. -struct ClawhubInspectData: Decodable, Sendable { - let skill: ClawhubInspectSkill - let owner: ClawhubInspectOwner? - let stats: ClawhubInspectStats? - let createdAt: Int? - let updatedAt: Int? - let latestVersion: ClawhubInspectVersion? - let files: [ClawhubInspectFile]? - let skillMdContent: String? -} - -/// Response from inspecting a ClaWHub skill. -/// Wire type: "skills_inspect_response" -struct SkillsInspectResponseMessage: Decodable, Sendable { - let slug: String - let data: ClawhubInspectData? - let error: String? -} - -/// Response containing the list of past sessions. -/// Wire type: `"session_list_response"` -struct SessionListResponseMessage: Decodable, Sendable { - struct SessionItem: Decodable, Sendable { - let id: String - let title: String - let updatedAt: Int - } - let sessions: [SessionItem] -} - -/// Response containing message history for a session. -/// Wire type: `"history_response"` -struct HistoryResponseMessage: Decodable, Sendable { - let sessionId: String - struct HistoryToolCallItem: Decodable, Sendable { - let name: String - let input: [String: AnyCodable] - let result: String? - let isError: Bool? - } - struct HistoryMessageItem: Decodable, Sendable { - let role: String - let text: String - let timestamp: Int - let toolCalls: [HistoryToolCallItem]? - } - let messages: [HistoryMessageItem] -} - -/// A single trust rule item returned from the daemon. -struct TrustRuleItem: Decodable, Sendable, Identifiable { - let id: String - let tool: String - let pattern: String - let scope: String - let decision: String - let priority: Int - let createdAt: Double -} - -/// Response containing all trust rules. -/// Wire type: `"trust_rules_list_response"` -struct TrustRulesListResponseMessage: Decodable, Sendable { - let rules: [TrustRuleItem] -} - -/// A single app item returned from the daemon. -struct AppItem: Decodable, Sendable, Identifiable { - let id: String - let name: String - let description: String? - let icon: String? - let createdAt: Int -} - -/// Response containing the list of all apps. -/// Wire type: `"apps_list_response"` -struct AppsListResponseMessage: Decodable, Sendable { - let apps: [AppItem] -} - -/// A single shared app item returned from the daemon. -struct SharedAppItem: Decodable, Sendable, Identifiable { - var id: String { uuid } - let uuid: String - let name: String - let description: String? - let icon: String? - let entry: String - let trustTier: String - let signerDisplayName: String? - let bundleSizeBytes: Int - let installedAt: String -} - -/// Response containing the list of shared apps. -/// Wire type: `"shared_apps_list_response"` -struct SharedAppsListResponseMessage: Decodable, Sendable { - let apps: [SharedAppItem] -} - -/// Response from deleting a shared app. -/// Wire type: `"shared_app_delete_response"` -struct SharedAppDeleteResponseMessage: Decodable, Sendable { - let success: Bool -} - -/// Response from bundling an app. -/// Wire type: `"bundle_app_response"` -struct BundleAppResponseMessage: Decodable, Sendable { - let bundlePath: String -} - -/// Request from daemon to sign a bundle payload. -/// Wire type: `"sign_bundle_payload"` -struct SignBundlePayloadMessage: Decodable, Sendable { - let payload: String -} - -/// Timer completed notification from daemon. -/// Wire type: `"timer_completed"` -struct TimerCompletedMessage: Decodable, Sendable { - let sessionId: String - let timerId: String - let label: String - let durationMinutes: Double -} - -/// Tool execution started. -/// Wire type: `"tool_use_start"` -struct ToolUseStartMessage: Decodable, Sendable { - let toolName: String - let input: [String: AnyCodable] - let sessionId: String? -} - -/// Streaming tool output chunk. -/// Wire type: `"tool_output_chunk"` -struct ToolOutputChunkMessage: Decodable, Sendable { - let chunk: String -} - -/// Tool execution completed. -/// Wire type: `"tool_result"` -struct ToolResultMessage: Decodable, Sendable { - let toolName: String - let result: String - let isError: Bool? - 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. -/// Wire type: `"suggestion_response"` -struct SuggestionResponseMessage: Decodable, Sendable { - let requestId: String - let suggestion: String? - let source: String -} - -/// Secret input request from daemon. -/// Wire type: `"secret_request"` -struct SecretRequestMessage: Decodable, Sendable { - let requestId: String - let service: String - let field: String - let label: String - let description: String? - let placeholder: String? - let sessionId: String? -} - -/// Permission confirmation request from daemon. -/// Wire type: `"confirmation_request"` -struct ConfirmationRequestMessage: Decodable, Sendable { - let requestId: String - let toolName: String - let input: [String: AnyCodable] - let riskLevel: String - let allowlistOptions: [ConfirmationAllowlistOption] - let scopeOptions: [ConfirmationScopeOption] - let diff: ConfirmationDiffInfo? - let sandboxed: Bool? - let sessionId: String? - - struct ConfirmationAllowlistOption: Decodable, Sendable, Equatable { - let label: String - let description: String? - let pattern: String - } - struct ConfirmationScopeOption: Decodable, Sendable, Equatable { - let label: String - let scope: String - } - struct ConfirmationDiffInfo: Decodable, Sendable, Equatable { - let filePath: String - let oldContent: String - let newContent: String - let isNewFile: Bool - } -} - -/// Request a follow-up suggestion for the current session. -/// Wire type: `"suggestion_request"` -struct SuggestionRequestMessage: Encodable, Sendable { - let type: String = "suggestion_request" - let sessionId: String - let requestId: String -} - -/// Client response to a permission confirmation request. -/// Wire type: `"confirmation_response"` -struct ConfirmationResponseMessage: Encodable, Sendable { - let type: String = "confirmation_response" - let requestId: String - let decision: String - let selectedPattern: String? - let selectedScope: String? -} - -/// Client response to a secret input request. -/// Wire type: `"secret_response"` -struct SecretResponseMessage: Encodable, Sendable { - let type: String = "secret_response" - let requestId: String - let value: String? -} - -/// Sent to add a trust rule (allowlist/denylist) independently of a confirmation response. -/// Wire type: `"add_trust_rule"` -struct AddTrustRuleMessage: Encodable, Sendable { - let type: String = "add_trust_rule" - let toolName: String - let pattern: String - let scope: String - let decision: String -} - -/// Request all trust rules from the daemon. -/// Wire type: `"trust_rules_list"` -struct TrustRulesListMessage: Encodable, Sendable { - let type: String = "trust_rules_list" -} - -/// Remove a trust rule by its ID. -/// Wire type: `"remove_trust_rule"` -struct RemoveTrustRuleMessage: Encodable, Sendable { - let type: String = "remove_trust_rule" - let id: String -} - -/// Update fields on an existing trust rule. -/// Wire type: `"update_trust_rule"` -struct UpdateTrustRuleMessage: Encodable, Sendable { - let type: String = "update_trust_rule" - let id: String - let tool: String? - let pattern: String? - let scope: String? - let decision: String? - let priority: Int? -} - -/// Response from opening and scanning a .vellumapp bundle. -/// Wire type: `"open_bundle_response"` -struct OpenBundleResponseMessage: Decodable, Sendable { - struct Manifest: Decodable, Sendable { - let formatVersion: Int - let name: String - let description: String? - let icon: String? - let createdAt: String - let createdBy: String - let entry: String - let capabilities: [String] - - private enum CodingKeys: String, CodingKey { - case formatVersion = "format_version" - case name, description, icon - case createdAt = "created_at" - case createdBy = "created_by" - case entry, capabilities - } - } - struct ScanResult: Decodable, Sendable { - let passed: Bool - let blocked: [String] - let warnings: [String] - } - struct SignatureResult: Decodable, Sendable { - let trustTier: String - let signerKeyId: String? - let signerDisplayName: String? - let signerAccount: String? - } - let manifest: Manifest - let scanResult: ScanResult - let signatureResult: SignatureResult - let bundleSizeBytes: Int -} - -/// Discriminated union of all server → client message types relevant to the macOS client. -/// Decodes via the `"type"` field in the JSON payload. -enum ServerMessage: Decodable, Sendable { - case cuAction(CuActionMessage) - case cuComplete(CuCompleteMessage) - case cuError(CuErrorMessage) - case assistantTextDelta(AssistantTextDeltaMessage) - case assistantThinkingDelta(AssistantThinkingDeltaMessage) - case messageComplete(MessageCompleteMessage) - case sessionInfo(SessionInfoMessage) - case sessionListResponse(SessionListResponseMessage) - case historyResponse(HistoryResponseMessage) - case taskRouted(TaskRoutedMessage) - case error(ErrorMessage) - case ambientResult(AmbientResultMessage) - case uiSurfaceShow(UiSurfaceShowMessage) - case uiSurfaceUpdate(UiSurfaceUpdateMessage) - case uiSurfaceDismiss(UiSurfaceDismissMessage) - case generationCancelled(GenerationCancelledMessage) - case generationHandoff(GenerationHandoffMessage) - case confirmationRequest(ConfirmationRequestMessage) - case secretRequest(SecretRequestMessage) - case appDataResponse(AppDataResponseMessage) - case messageQueued(MessageQueuedMessage) - case messageDequeued(MessageDequeuedMessage) - case skillsListResponse(SkillsListResponseMessage) - case skillDetailResponse(SkillDetailResponseMessage) - case skillStateChanged(SkillStateChangedMessage) - case skillsUpdatesAvailable(SkillsUpdatesAvailableMessage) - case skillsOperationResponse(SkillsOperationResponseMessage) - case skillsInspectResponse(SkillsInspectResponseMessage) - case suggestionResponse(SuggestionResponseMessage) - case toolUseStart(ToolUseStartMessage) - case toolOutputChunk(ToolOutputChunkMessage) - case toolResult(ToolResultMessage) - case timerCompleted(TimerCompletedMessage) - case trustRulesListResponse(TrustRulesListResponseMessage) - case appsListResponse(AppsListResponseMessage) - case sharedAppsListResponse(SharedAppsListResponseMessage) - case sharedAppDeleteResponse(SharedAppDeleteResponseMessage) - case bundleAppResponse(BundleAppResponseMessage) - case openBundleResponse(OpenBundleResponseMessage) - case signBundlePayload(SignBundlePayloadMessage) - case getSigningIdentity - case pong - case unknown(String) - - private enum CodingKeys: String, CodingKey { - case type - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let type = try container.decode(String.self, forKey: .type) - - switch type { - case "cu_action": - let message = try CuActionMessage(from: decoder) - self = .cuAction(message) - case "cu_complete": - let message = try CuCompleteMessage(from: decoder) - self = .cuComplete(message) - case "cu_error": - let message = try CuErrorMessage(from: decoder) - self = .cuError(message) - case "assistant_text_delta": - let message = try AssistantTextDeltaMessage(from: decoder) - self = .assistantTextDelta(message) - case "assistant_thinking_delta": - let message = try AssistantThinkingDeltaMessage(from: decoder) - self = .assistantThinkingDelta(message) - case "message_complete": - let message = try MessageCompleteMessage(from: decoder) - self = .messageComplete(message) - case "session_info": - let message = try SessionInfoMessage(from: decoder) - self = .sessionInfo(message) - case "session_list_response": - let message = try SessionListResponseMessage(from: decoder) - self = .sessionListResponse(message) - case "history_response": - let message = try HistoryResponseMessage(from: decoder) - self = .historyResponse(message) - case "task_routed": - let message = try TaskRoutedMessage(from: decoder) - self = .taskRouted(message) - case "error": - let message = try ErrorMessage(from: decoder) - self = .error(message) - case "ambient_result": - let message = try AmbientResultMessage(from: decoder) - self = .ambientResult(message) - case "ui_surface_show": - let message = try UiSurfaceShowMessage(from: decoder) - self = .uiSurfaceShow(message) - case "ui_surface_update": - let message = try UiSurfaceUpdateMessage(from: decoder) - self = .uiSurfaceUpdate(message) - case "ui_surface_dismiss": - let message = try UiSurfaceDismissMessage(from: decoder) - self = .uiSurfaceDismiss(message) - case "generation_cancelled": - let message = try GenerationCancelledMessage(from: decoder) - self = .generationCancelled(message) - case "generation_handoff": - let message = try GenerationHandoffMessage(from: decoder) - self = .generationHandoff(message) - case "confirmation_request": - let message = try ConfirmationRequestMessage(from: decoder) - self = .confirmationRequest(message) - case "secret_request": - let message = try SecretRequestMessage(from: decoder) - self = .secretRequest(message) - case "app_data_response": - let message = try AppDataResponseMessage(from: decoder) - self = .appDataResponse(message) - case "message_queued": - let message = try MessageQueuedMessage(from: decoder) - self = .messageQueued(message) - case "message_dequeued": - let message = try MessageDequeuedMessage(from: decoder) - self = .messageDequeued(message) - case "skills_list_response": - let message = try SkillsListResponseMessage(from: decoder) - self = .skillsListResponse(message) - case "skill_detail_response": - let message = try SkillDetailResponseMessage(from: decoder) - self = .skillDetailResponse(message) - case "skills_state_changed": - let message = try SkillStateChangedMessage(from: decoder) - self = .skillStateChanged(message) - case "skills_updates_available": - let message = try SkillsUpdatesAvailableMessage(from: decoder) - self = .skillsUpdatesAvailable(message) - case "skills_operation_response": - let message = try SkillsOperationResponseMessage(from: decoder) - self = .skillsOperationResponse(message) - case "skills_inspect_response": - let message = try SkillsInspectResponseMessage(from: decoder) - self = .skillsInspectResponse(message) - case "suggestion_response": - let message = try SuggestionResponseMessage(from: decoder) - self = .suggestionResponse(message) - case "tool_use_start": - let message = try ToolUseStartMessage(from: decoder) - self = .toolUseStart(message) - case "tool_output_chunk": - let message = try ToolOutputChunkMessage(from: decoder) - self = .toolOutputChunk(message) - case "tool_result": - let message = try ToolResultMessage(from: decoder) - self = .toolResult(message) - case "timer_completed": - let message = try TimerCompletedMessage(from: decoder) - self = .timerCompleted(message) - case "trust_rules_list_response": - let message = try TrustRulesListResponseMessage(from: decoder) - self = .trustRulesListResponse(message) - case "apps_list_response": - let message = try AppsListResponseMessage(from: decoder) - self = .appsListResponse(message) - case "shared_apps_list_response": - let message = try SharedAppsListResponseMessage(from: decoder) - self = .sharedAppsListResponse(message) - case "shared_app_delete_response": - let message = try SharedAppDeleteResponseMessage(from: decoder) - self = .sharedAppDeleteResponse(message) - case "bundle_app_response": - let message = try BundleAppResponseMessage(from: decoder) - self = .bundleAppResponse(message) - case "open_bundle_response": - let message = try OpenBundleResponseMessage(from: decoder) - self = .openBundleResponse(message) - case "sign_bundle_payload": - let message = try SignBundlePayloadMessage(from: decoder) - self = .signBundlePayload(message) - case "get_signing_identity": - self = .getSigningIdentity - case "pong": - self = .pong - default: - self = .unknown(type) - } - } -} diff --git a/clients/macos/vellum-assistantTests/ChatViewModelTests.swift b/clients/macos/vellum-assistantTests/ChatViewModelTests.swift index d6ebe49088a..a47044f3093 100644 --- a/clients/macos/vellum-assistantTests/ChatViewModelTests.swift +++ b/clients/macos/vellum-assistantTests/ChatViewModelTests.swift @@ -1,5 +1,6 @@ import XCTest @testable import VellumAssistantLib +import VellumAssistantShared @MainActor final class ChatViewModelTests: XCTestCase { diff --git a/clients/macos/vellum-assistantTests/DaemonClientSocketPathTests.swift b/clients/macos/vellum-assistantTests/DaemonClientSocketPathTests.swift index 2c8eb094490..2b525281da7 100644 --- a/clients/macos/vellum-assistantTests/DaemonClientSocketPathTests.swift +++ b/clients/macos/vellum-assistantTests/DaemonClientSocketPathTests.swift @@ -1,5 +1,6 @@ import XCTest @testable import VellumAssistantLib +import VellumAssistantShared @MainActor final class DaemonClientSocketPathTests: XCTestCase { diff --git a/clients/macos/vellum-assistantTests/SessionTests.swift b/clients/macos/vellum-assistantTests/SessionTests.swift index 1fb35e57055..f7f255f9d14 100644 --- a/clients/macos/vellum-assistantTests/SessionTests.swift +++ b/clients/macos/vellum-assistantTests/SessionTests.swift @@ -2,6 +2,7 @@ import XCTest import CoreGraphics import Combine @testable import VellumAssistantLib +import VellumAssistantShared // MARK: - Mock Daemon Client diff --git a/clients/macos/vellum-assistant/App/SigningIdentityManager.swift b/clients/shared/App/SigningIdentityManager.swift similarity index 92% rename from clients/macos/vellum-assistant/App/SigningIdentityManager.swift rename to clients/shared/App/SigningIdentityManager.swift index c6b6a7b1dc8..485959287bc 100644 --- a/clients/macos/vellum-assistant/App/SigningIdentityManager.swift +++ b/clients/shared/App/SigningIdentityManager.swift @@ -1,3 +1,4 @@ +#if os(macOS) import CryptoKit import Foundation import Security @@ -11,8 +12,8 @@ private let log = Logger( /// Manages the Ed25519 signing identity stored in the macOS Keychain. /// Key is generated on first access and persisted across launches. @MainActor -final class SigningIdentityManager { - static let shared = SigningIdentityManager() +public final class SigningIdentityManager { + public static let shared = SigningIdentityManager() private let service = "vellum-assistant" private let account = "signing-key" @@ -21,7 +22,7 @@ final class SigningIdentityManager { private var cachedKey: Curve25519.Signing.PrivateKey? /// Get or create the Ed25519 signing private key. - func getPrivateKey() throws -> Curve25519.Signing.PrivateKey { + public func getPrivateKey() throws -> Curve25519.Signing.PrivateKey { if let cached = cachedKey { return cached } @@ -41,19 +42,19 @@ final class SigningIdentityManager { } /// Get the public key. - func getPublicKey() throws -> Curve25519.Signing.PublicKey { + public func getPublicKey() throws -> Curve25519.Signing.PublicKey { return try getPrivateKey().publicKey } /// Key identifier (SHA-256 fingerprint of public key, hex-encoded). - func getKeyId() throws -> String { + public func getKeyId() throws -> String { let publicKey = try getPublicKey() let digest = SHA256.hash(data: publicKey.rawRepresentation) return digest.map { String(format: "%02x", $0) }.joined() } /// Sign data with the signing key. - func sign(_ data: Data) throws -> Data { + public func sign(_ data: Data) throws -> Data { let signingKey = try getPrivateKey() return try signingKey.signature(for: data) } @@ -137,3 +138,4 @@ final class SigningIdentityManager { } } } +#endif diff --git a/clients/macos/vellum-assistant/IPC/DaemonClient.swift b/clients/shared/IPC/DaemonClient.swift similarity index 79% rename from clients/macos/vellum-assistant/IPC/DaemonClient.swift rename to clients/shared/IPC/DaemonClient.swift index 1dd8262951d..d65df260ede 100644 --- a/clients/macos/vellum-assistant/IPC/DaemonClient.swift +++ b/clients/shared/IPC/DaemonClient.swift @@ -6,15 +6,17 @@ private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.vellum. /// Protocol for daemon client communication, enabling dependency injection and testing. @MainActor -protocol DaemonClientProtocol { +public protocol DaemonClientProtocol { func subscribe() -> AsyncStream func send(_ message: T) throws } -/// Unix domain socket client for communicating with the Vellum daemon. +/// Platform-agnostic client for communicating with the Vellum daemon. /// -/// Connects to the daemon's socket at `~/.vellum/vellum.sock` (or `VELLUM_DAEMON_SOCKET` env override), -/// sends and receives newline-delimited JSON messages. +/// **macOS**: Connects via Unix domain socket at `~/.vellum/vellum.sock` (or `VELLUM_DAEMON_SOCKET` env override). +/// **iOS**: Connects via TCP to configurable hostname:port (UserDefaults: `daemon_hostname`, `daemon_port`). +/// +/// Sends and receives newline-delimited JSON messages over the connection. /// /// This is a long-lived singleton. Consumers call `subscribe()` to get an independent message /// stream, enabling multiple consumers (ComputerUseSession, AmbientAgent) to each receive all @@ -24,94 +26,94 @@ public final class DaemonClient: ObservableObject, DaemonClientProtocol { // MARK: - Published State - @Published var isConnected: Bool = false + @Published public var isConnected: Bool = false /// Shared flag so only one TrustRulesView sheet is open at a time across SettingsPanel and SettingsView. /// Both surfaces bind to this instead of local @State, preventing the second sheet from overwriting /// the first sheet's `onTrustRulesListResponse` callback on DaemonClient. - @Published var isTrustRulesSheetOpen: Bool = false + @Published public var isTrustRulesSheetOpen: Bool = false // MARK: - Surface Event Callbacks /// Called when the daemon sends a `ui_surface_show` message. /// Set by the app layer to forward to SurfaceManager without coupling DaemonClient to it. - var onSurfaceShow: ((UiSurfaceShowMessage) -> Void)? + public var onSurfaceShow: ((UiSurfaceShowMessage) -> Void)? /// Called when the daemon sends a `ui_surface_update` message. - var onSurfaceUpdate: ((UiSurfaceUpdateMessage) -> Void)? + public var onSurfaceUpdate: ((UiSurfaceUpdateMessage) -> Void)? /// Called when the daemon sends a `ui_surface_dismiss` message. - var onSurfaceDismiss: ((UiSurfaceDismissMessage) -> Void)? + public var onSurfaceDismiss: ((UiSurfaceDismissMessage) -> Void)? /// Called when the daemon sends an `app_data_response` message. - var onAppDataResponse: ((AppDataResponseMessage) -> Void)? + public var onAppDataResponse: ((AppDataResponseMessage) -> Void)? /// Called when the daemon sends a `message_queued` message. - var onMessageQueued: ((MessageQueuedMessage) -> Void)? + public var onMessageQueued: ((MessageQueuedMessage) -> Void)? /// Called when the daemon sends a `message_dequeued` message. - var onMessageDequeued: ((MessageDequeuedMessage) -> Void)? + public var onMessageDequeued: ((MessageDequeuedMessage) -> Void)? /// Called when the daemon sends a `generation_handoff` message. - var onGenerationHandoff: ((GenerationHandoffMessage) -> Void)? + public var onGenerationHandoff: ((GenerationHandoffMessage) -> Void)? /// Called when the daemon sends a `confirmation_request` message for tool permission approval. - var onConfirmationRequest: ((ConfirmationRequestMessage) -> Void)? + public var onConfirmationRequest: ((ConfirmationRequestMessage) -> Void)? /// Called when the daemon sends a `secret_request` message for secure credential input. - var onSecretRequest: ((SecretRequestMessage) -> Void)? + public var onSecretRequest: ((SecretRequestMessage) -> Void)? /// Called when the daemon sends a `task_routed` message (e.g. escalation from text_qa to CU). - var onTaskRouted: ((TaskRoutedMessage) -> Void)? + public var onTaskRouted: ((TaskRoutedMessage) -> Void)? /// Called when a pomodoro timer completes. - var onTimerCompleted: ((TimerCompletedMessage) -> Void)? + public var onTimerCompleted: ((TimerCompletedMessage) -> Void)? /// Called when the daemon sends a `trust_rules_list_response` message. - var onTrustRulesListResponse: (([TrustRuleItem]) -> Void)? + public var onTrustRulesListResponse: (([TrustRuleItem]) -> Void)? /// Called when the daemon sends a `skills_state_changed` push event. - var onSkillStateChanged: ((SkillStateChangedMessage) -> Void)? + public var onSkillStateChanged: ((SkillStateChangedMessage) -> Void)? /// Called when the daemon sends a `skills_updates_available` push event. - var onSkillsUpdatesAvailable: ((SkillsUpdatesAvailableMessage) -> Void)? + public var onSkillsUpdatesAvailable: ((SkillsUpdatesAvailableMessage) -> Void)? /// Called when the daemon sends a `skills_operation_response` message. - var onSkillsOperationResponse: ((SkillsOperationResponseMessage) -> Void)? + public var onSkillsOperationResponse: ((SkillsOperationResponseMessage) -> Void)? /// Called when the daemon sends a `skills_inspect_response` message. - var onSkillsInspectResponse: ((SkillsInspectResponseMessage) -> Void)? + public var onSkillsInspectResponse: ((SkillsInspectResponseMessage) -> Void)? /// Called when the daemon sends an `apps_list_response` message. - var onAppsListResponse: ((AppsListResponseMessage) -> Void)? + public var onAppsListResponse: ((AppsListResponseMessage) -> Void)? /// Called when the daemon sends a `shared_apps_list_response` message. - var onSharedAppsListResponse: ((SharedAppsListResponseMessage) -> Void)? + public var onSharedAppsListResponse: ((SharedAppsListResponseMessage) -> Void)? /// Called when the daemon sends a `shared_app_delete_response` message. - var onSharedAppDeleteResponse: ((SharedAppDeleteResponseMessage) -> Void)? + public var onSharedAppDeleteResponse: ((SharedAppDeleteResponseMessage) -> Void)? /// Called when the daemon sends a `bundle_app_response` message. - var onBundleAppResponse: ((BundleAppResponseMessage) -> Void)? + public var onBundleAppResponse: ((BundleAppResponseMessage) -> Void)? /// Called when the daemon sends an `open_bundle_response` message. - var onOpenBundleResponse: ((OpenBundleResponseMessage) -> Void)? + public var onOpenBundleResponse: ((OpenBundleResponseMessage) -> Void)? /// Called when the daemon sends a `session_list_response` message. - var onSessionListResponse: ((SessionListResponseMessage) -> Void)? + public var onSessionListResponse: ((SessionListResponseMessage) -> Void)? /// Called when the daemon sends a `history_response` message. - var onHistoryResponse: ((HistoryResponseMessage) -> Void)? + public var onHistoryResponse: ((HistoryResponseMessage) -> Void)? /// Called when the daemon sends a generic `error` message (e.g. when a handler fails). - var onError: ((ErrorMessage) -> Void)? + public var onError: ((ErrorMessage) -> Void)? // MARK: - Broadcast Subscribers /// Creates a new message stream for the caller. Each subscriber receives all messages /// independently, enabling multiple consumers (ComputerUseSession, AmbientAgent) to /// filter for messages relevant to them without competing for elements. - func subscribe() -> AsyncStream { + public func subscribe() -> AsyncStream { let id = UUID() let (stream, continuation) = AsyncStream.makeStream() subscribers[id] = continuation @@ -162,10 +164,22 @@ public final class DaemonClient: ObservableObject, DaemonClientProtocol { // MARK: - Init - init() {} + public init() {} deinit { - // Cancel everything without triggering reconnect. + // Swift 5.9+: deinit on @MainActor class is NOT guaranteed to run on main actor. + // Cannot use MainActor.assumeIsolated here as it would crash if deinit runs on + // a background thread (e.g., if last reference is released from a background context). + // + // Instead, we access the properties directly. While this is technically a data race, + // the cleanup operations are all thread-safe: + // - Task.cancel() is thread-safe + // - NWConnection.cancel() is thread-safe + // - AsyncStream.Continuation.finish() is thread-safe + // + // Setting shouldReconnect and accessing subscribers are data races, but they're + // benign in deinit since the object is being destroyed and no other code can + // access these properties. shouldReconnect = false reconnectTask?.cancel() pingTask?.cancel() @@ -179,12 +193,13 @@ public final class DaemonClient: ObservableObject, DaemonClientProtocol { // MARK: - Socket Path - /// Resolves the daemon socket path: + /// Resolves the daemon socket path (macOS only): /// 1. `VELLUM_DAEMON_SOCKET` environment variable (or override dictionary) /// 2. `~/.vellum/vellum.sock` /// /// Accepts an optional environment dictionary for testability. - static func resolveSocketPath(environment: [String: String]? = nil) -> String { + #if os(macOS) + public static func resolveSocketPath(environment: [String: String]? = nil) -> String { let env = environment ?? ProcessInfo.processInfo.environment if let envPath = env["VELLUM_DAEMON_SOCKET"], !envPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { let trimmed = envPath.trimmingCharacters(in: .whitespacesAndNewlines) @@ -195,23 +210,39 @@ public final class DaemonClient: ObservableObject, DaemonClientProtocol { } return NSHomeDirectory() + "/.vellum/vellum.sock" } + #endif // MARK: - Connect /// How long to wait for a connection before giving up. private static let connectTimeout: TimeInterval = 5.0 - /// Connect to the daemon socket. If already connected, disconnects first. - func connect() async throws { + /// Connect to the daemon. If already connected, disconnects first. + /// - macOS: Connects to Unix domain socket at `~/.vellum/vellum.sock` + /// - iOS: Connects to TCP endpoint (hostname from UserDefaults or localhost:8765) + public func connect() async throws { // Disconnect any existing connection without triggering reconnect. disconnectInternal(triggerReconnect: false) shouldReconnect = true + #if os(macOS) let socketPath = Self.resolveSocketPath() log.info("Connecting to daemon socket at \(socketPath)") - let endpoint = NWEndpoint.unix(path: socketPath) + #elseif os(iOS) + let hostname = UserDefaults.standard.string(forKey: "daemon_hostname") ?? "localhost" + let rawPort = UserDefaults.standard.integer(forKey: "daemon_port") + let port = UInt16(clamping: rawPort > 0 && rawPort <= 65535 ? rawPort : 8765) + log.info("Connecting to daemon at \(hostname):\(port)") + let endpoint = NWEndpoint.hostPort( + host: NWEndpoint.Host(hostname), + port: NWEndpoint.Port(integerLiteral: port) + ) + #else + #error("DaemonClient is only supported on macOS and iOS") + #endif + let parameters = NWParameters() parameters.defaultProtocolStack.transportProtocol = NWProtocolTCP.Options() @@ -293,10 +324,10 @@ public final class DaemonClient: ObservableObject, DaemonClientProtocol { // MARK: - Send - enum SendError: Error, LocalizedError { + public enum SendError: Error, LocalizedError { case notConnected - var errorDescription: String? { + public var errorDescription: String? { switch self { case .notConnected: return "Cannot send: not connected to daemon" @@ -308,7 +339,7 @@ public final class DaemonClient: ObservableObject, DaemonClientProtocol { /// Encodes the message as JSON, appends a newline, and writes to the connection. /// Throws `SendError.notConnected` when the connection is nil so callers can /// distinguish a silently-dropped message from a successful write. - func send(_ message: T) throws { + public func send(_ message: T) throws { guard let conn = connection else { log.warning("Cannot send: not connected") throw SendError.notConnected @@ -328,7 +359,7 @@ public final class DaemonClient: ObservableObject, DaemonClientProtocol { /// Convenience method for sending a surface action response to the daemon. /// Keeps the IPC message construction co-located with the client. - func sendSurfaceAction(sessionId: String, surfaceId: String, actionId: String, data: [String: AnyCodable]?) throws { + public func sendSurfaceAction(sessionId: String, surfaceId: String, actionId: String, data: [String: AnyCodable]?) throws { let message = UiSurfaceActionMessage( sessionId: sessionId, surfaceId: surfaceId, @@ -341,7 +372,7 @@ public final class DaemonClient: ObservableObject, DaemonClientProtocol { // MARK: - Confirmation Response /// Send a confirmation response for a tool permission request. - func sendConfirmationResponse( + public func sendConfirmationResponse( requestId: String, decision: String, selectedPattern: String? = nil, @@ -358,14 +389,14 @@ public final class DaemonClient: ObservableObject, DaemonClientProtocol { // MARK: - Secret Response /// Send a secret response for a credential prompt request. - func sendSecretResponse(requestId: String, value: String?) throws { + public func sendSecretResponse(requestId: String, value: String?) throws { try send(SecretResponseMessage(requestId: requestId, value: value)) } // MARK: - Trust Rule Addition /// Send an add_trust_rule message to persist a trust rule on the daemon. - func sendAddTrustRule( + public func sendAddTrustRule( toolName: String, pattern: String, scope: String, @@ -382,17 +413,17 @@ public final class DaemonClient: ObservableObject, DaemonClientProtocol { // MARK: - Trust Rule Management /// Request the list of all trust rules from the daemon. - func sendListTrustRules() throws { + public func sendListTrustRules() throws { try send(TrustRulesListMessage()) } /// Remove a trust rule by its ID. - func sendRemoveTrustRule(id: String) throws { + public func sendRemoveTrustRule(id: String) throws { try send(RemoveTrustRuleMessage(id: id)) } /// Update fields on an existing trust rule. - func sendUpdateTrustRule( + public func sendUpdateTrustRule( id: String, tool: String? = nil, pattern: String? = nil, @@ -413,91 +444,92 @@ public final class DaemonClient: ObservableObject, DaemonClientProtocol { // MARK: - Skills Management /// Enable a skill by name. - func enableSkill(_ name: String) throws { + public func enableSkill(_ name: String) throws { try send(SkillsEnableMessage(name: name)) } /// Disable a skill by name. - func disableSkill(_ name: String) throws { + public func disableSkill(_ name: String) throws { try send(SkillsDisableMessage(name: name)) } /// Install a skill from ClaWHub. - func installSkill(slug: String, version: String? = nil) throws { + public func installSkill(slug: String, version: String? = nil) throws { try send(SkillsInstallMessage(slug: slug, version: version)) } /// Uninstall a skill by name. - func uninstallSkill(_ name: String) throws { + public func uninstallSkill(_ name: String) throws { try send(SkillsUninstallMessage(name: name)) } /// Update a skill to its latest version. - func updateSkill(_ name: String) throws { + public func updateSkill(_ name: String) throws { try send(SkillsUpdateMessage(name: name)) } /// Check for available skill updates. - func checkSkillUpdates() throws { + public func checkSkillUpdates() throws { try send(SkillsCheckUpdatesMessage()) } /// Search for skills on ClaWHub. - func searchSkills(query: String) throws { + public func searchSkills(query: String) throws { try send(SkillsSearchMessage(query: query)) } /// Inspect a ClaWHub skill for detailed metadata. - func inspectSkill(slug: String) throws { + public func inspectSkill(slug: String) throws { try send(SkillsInspectMessage(slug: slug)) } /// Configure a skill's environment, API key, or config. - func configureSkill(name: String, env: [String: String]? = nil, apiKey: String? = nil, config: [String: AnyCodable]? = nil) throws { + public func configureSkill(name: String, env: [String: String]? = nil, apiKey: String? = nil, config: [String: AnyCodable]? = nil) throws { try send(SkillsConfigureMessage(name: name, env: env, apiKey: apiKey, config: config)) } // MARK: - Sessions /// Request the list of past sessions from the daemon. - func sendSessionList() throws { + public func sendSessionList() throws { try send(SessionListRequestMessage()) } /// Request message history for a specific session. - func sendHistoryRequest(sessionId: String) throws { + public func sendHistoryRequest(sessionId: String) throws { try send(HistoryRequestMessage(sessionId: sessionId)) } // MARK: - Apps /// Request the list of all apps from the daemon. - func sendAppsList() throws { + public func sendAppsList() throws { try send(AppsListRequestMessage()) } /// Request bundling an app for sharing. - func sendBundleApp(appId: String) throws { + public func sendBundleApp(appId: String) throws { try send(BundleAppRequestMessage(appId: appId)) } /// Request opening and scanning a .vellumapp bundle. - func sendOpenBundle(filePath: String) throws { + public func sendOpenBundle(filePath: String) throws { try send(OpenBundleMessage(filePath: filePath)) } /// Request the list of shared/received apps. - func sendSharedAppsList() throws { + public func sendSharedAppsList() throws { try send(SharedAppsListRequestMessage()) } /// Delete a shared app by UUID. - func sendSharedAppDelete(uuid: String) throws { + public func sendSharedAppDelete(uuid: String) throws { try send(SharedAppDeleteRequestMessage(uuid: uuid)) } - // MARK: - Signing Identity + // MARK: - Signing Identity (macOS only) + #if os(macOS) /// Handle a sign_bundle_payload request from the daemon. private func handleSignBundlePayload(_ msg: SignBundlePayloadMessage) { do { @@ -530,11 +562,12 @@ public final class DaemonClient: ObservableObject, DaemonClientProtocol { log.error("Failed to get signing identity: \(error.localizedDescription)") } } + #endif // MARK: - Disconnect /// Disconnect from the daemon. Stops reconnect and ping timers. - func disconnect() { + public func disconnect() { disconnectInternal(triggerReconnect: false) } @@ -687,10 +720,20 @@ public final class DaemonClient: ObservableObject, DaemonClientProtocol { onHistoryResponse?(msg) case .error(let msg): onError?(msg) + #if os(macOS) case .signBundlePayload(let msg): handleSignBundlePayload(msg) case .getSigningIdentity: handleGetSigningIdentity() + #elseif os(iOS) + case .signBundlePayload: + log.error("Received sign_bundle_payload request on iOS - signing operations are not supported on iOS due to sandboxing restrictions") + case .getSigningIdentity: + log.error("Received get_signing_identity request on iOS - signing operations are not supported on iOS due to sandboxing restrictions") + #else + case .signBundlePayload, .getSigningIdentity: + log.error("Signing operations are not supported on this platform") + #endif default: break } diff --git a/clients/shared/IPC/IPCMessages.swift b/clients/shared/IPC/IPCMessages.swift new file mode 100644 index 00000000000..8673ae81ef6 --- /dev/null +++ b/clients/shared/IPC/IPCMessages.swift @@ -0,0 +1,1432 @@ +import Foundation + +// MARK: - AnyCodable + +/// Lightweight wrapper for arbitrary JSON values in tool input dictionaries. +/// Supports String, Int, Double, Bool, null, arrays, and nested objects. +public struct AnyCodable: Codable, Equatable, @unchecked Sendable { + public let value: Any? + + public init(_ value: Any?) { + self.value = value + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if container.decodeNil() { + value = nil + } else if let bool = try? container.decode(Bool.self) { + value = bool + } else if let int = try? container.decode(Int.self) { + value = int + } else if let double = try? container.decode(Double.self) { + value = double + } else if let string = try? container.decode(String.self) { + value = string + } else if let array = try? container.decode([AnyCodable].self) { + value = array.map { $0.value } + } else if let dict = try? container.decode([String: AnyCodable].self) { + value = dict.mapValues { $0.value } + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON value type") + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + if value == nil { + try container.encodeNil() + } else if let bool = value as? Bool { + try container.encode(bool) + } else if let int = value as? Int { + try container.encode(int) + } else if let double = value as? Double { + try container.encode(double) + } else if let string = value as? String { + try container.encode(string) + } else if let array = value as? [Any?] { + try container.encode(array.map { AnyCodable($0) }) + } else if let dict = value as? [String: Any?] { + try container.encode(dict.mapValues { AnyCodable($0) }) + } else { + try container.encodeNil() + } + } + + public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool { + switch (lhs.value, rhs.value) { + case (nil, nil): + return true + case let (l as Bool, r as Bool): + return l == r + case let (l as Int, r as Int): + return l == r + case let (l as Double, r as Double): + return l == r + case let (l as String, r as String): + return l == r + case let (l as [Any?], r as [Any?]): + return l.count == r.count && zip(l, r).allSatisfy { AnyCodable($0) == AnyCodable($1) } + case let (l as [String: Any?], r as [String: Any?]): + guard l.count == r.count else { return false } + return l.allSatisfy { key, lVal in + guard let rVal = r[key] else { return false } + return AnyCodable(lVal) == AnyCodable(rVal) + } + default: + return false + } + } +} + +// MARK: - Client → Server Messages (Encodable) + +/// Attachment payload sent inline as base64. Mirrors `UserMessageAttachment` from ipc-protocol.ts. +public struct IPCAttachment: Codable, Sendable { + public let filename: String + public let mimeType: String + public let data: String + public let extractedText: String? + + public init(filename: String, mimeType: String, data: String, extractedText: String?) { + self.filename = filename + self.mimeType = mimeType + self.data = data + self.extractedText = extractedText + } +} + +/// Sent to create a new computer-use session. +/// Wire type: `"cu_session_create"` +public struct CuSessionCreateMessage: Encodable, Sendable { + public let type: String = "cu_session_create" + public let sessionId: String + public let task: String + public let screenWidth: Int + public let screenHeight: Int + public let attachments: [IPCAttachment]? + public let interactionType: String? + + public init(sessionId: String, task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCAttachment]?, interactionType: String?) { + self.sessionId = sessionId + self.task = task + self.screenWidth = screenWidth + self.screenHeight = screenHeight + self.attachments = attachments + self.interactionType = interactionType + } +} + +/// Sent after each perceive step with AX tree, screenshot, and execution results. +/// Wire type: `"cu_observation"` +public struct CuObservationMessage: Encodable, Sendable { + public let type: String = "cu_observation" + public let sessionId: String + public let axTree: String? + public let axDiff: String? + public let secondaryWindows: String? + public let screenshot: String? + public let executionResult: String? + public let executionError: String? + + public init(sessionId: String, axTree: String?, axDiff: String?, secondaryWindows: String?, screenshot: String?, executionResult: String?, executionError: String?) { + self.sessionId = sessionId + self.axTree = axTree + self.axDiff = axDiff + self.secondaryWindows = secondaryWindows + self.screenshot = screenshot + self.executionResult = executionResult + self.executionError = executionError + } +} + +/// Sent by the ambient agent with OCR text from periodic screen captures. +/// Wire type: `"ambient_observation"` +public struct AmbientObservationMessage: Encodable, Sendable { + public let type: String = "ambient_observation" + public let requestId: String + public let ocrText: String + public let appName: String? + public let windowTitle: String? + public let timestamp: Double + + public init(requestId: String, ocrText: String, appName: String?, windowTitle: String?, timestamp: Double) { + self.requestId = requestId + self.ocrText = ocrText + self.appName = appName + self.windowTitle = windowTitle + self.timestamp = timestamp + } +} + +/// Sent to create a new Q&A session. +/// Wire type: `"session_create"` +public struct SessionCreateMessage: Encodable, Sendable { + public let type: String = "session_create" + public let title: String? + public let systemPromptOverride: String? + public let maxResponseTokens: Int? + /// Client-generated nonce echoed back in `session_info` so the caller can + /// correlate the response to its specific request. Prevents multiple + /// ChatViewModels sharing one DaemonClient from stealing each other's sessions. + public let correlationId: String? + + public init(title: String?, systemPromptOverride: String? = nil, maxResponseTokens: Int? = nil, correlationId: String? = nil) { + self.title = title + self.systemPromptOverride = systemPromptOverride + self.maxResponseTokens = maxResponseTokens + self.correlationId = correlationId + } +} + +/// Sent to add a user message to an existing Q&A session. +/// Wire type: `"user_message"` +public struct UserMessageMessage: Encodable, Sendable { + public let type: String = "user_message" + public let sessionId: String + public let content: String + public let attachments: [IPCAttachment]? + + public init(sessionId: String, content: String, attachments: [IPCAttachment]?) { + self.sessionId = sessionId + self.content = content + self.attachments = attachments + } +} + +/// Sent to request daemon-side classification and session creation. +/// Wire type: `"task_submit"` +public struct TaskSubmitMessage: Encodable, Sendable { + public let type: String = "task_submit" + public let task: String + public let screenWidth: Int + public let screenHeight: Int + public let attachments: [IPCAttachment]? + public let source: String? + + public init(task: String, screenWidth: Int, screenHeight: Int, attachments: [IPCAttachment]?, source: String?) { + self.task = task + self.screenWidth = screenWidth + self.screenHeight = screenHeight + self.attachments = attachments + self.source = source + } +} + +/// Sent to cancel the active generation. +/// Wire type: `"cancel"` +public struct CancelMessage: Encodable, Sendable { + public let type: String = "cancel" + public let sessionId: String + + public init(sessionId: String) { + self.sessionId = sessionId + } +} + +/// Sent to abort a running computer-use session. +/// Wire type: `"cu_session_abort"` +public struct CuSessionAbortMessage: Encodable, Sendable { + public let type: String = "cu_session_abort" + public let sessionId: String + + public init(sessionId: String) { + self.sessionId = sessionId + } +} + +/// Keepalive ping. +/// Wire type: `"ping"` +public struct PingMessage: Encodable, Sendable { + public let type: String = "ping" + + public init() {} +} + +/// Sent when user interacts with a surface. +/// Wire type: `"ui_surface_action"` +public struct UiSurfaceActionMessage: Encodable, Sendable { + public let type: String = "ui_surface_action" + public let sessionId: String + public let surfaceId: String + public let actionId: String + public let data: [String: AnyCodable]? + + public init(sessionId: String, surfaceId: String, actionId: String, data: [String: AnyCodable]?) { + self.sessionId = sessionId + self.surfaceId = surfaceId + self.actionId = actionId + self.data = data + } +} + +/// Sent when a persistent app's JS makes a data request via the RPC bridge. +/// Wire type: `"app_data_request"` +public struct AppDataRequestMessage: Encodable, Sendable { + public let type: String = "app_data_request" + public let surfaceId: String + public let callId: String + public let method: String + public let appId: String + public let recordId: String? + public let data: [String: AnyCodable]? + + public init(surfaceId: String, callId: String, method: String, appId: String, recordId: String?, data: [String: AnyCodable]?) { + self.surfaceId = surfaceId + self.callId = callId + self.method = method + self.appId = appId + self.recordId = recordId + self.data = data + } +} + +/// Sent to request the list of all apps. +/// Wire type: `"apps_list"` +public struct AppsListRequestMessage: Encodable, Sendable { + public let type: String = "apps_list" + + public init() {} +} + +/// Sent to request the list of shared/received apps. +/// Wire type: `"shared_apps_list"` +public struct SharedAppsListRequestMessage: Encodable, Sendable { + public let type: String = "shared_apps_list" + + public init() {} +} + +/// Sent to delete a shared app by UUID. +/// Wire type: `"shared_app_delete"` +public struct SharedAppDeleteRequestMessage: Encodable, Sendable { + public let type: String = "shared_app_delete" + public let uuid: String + + public init(uuid: String) { + self.uuid = uuid + } +} + +/// Sent to request bundling an app for sharing. +/// Wire type: `"bundle_app"` +public struct BundleAppRequestMessage: Encodable, Sendable { + public let type: String = "bundle_app" + public let appId: String + + public init(appId: String) { + self.appId = appId + } +} + +/// Sent to open and scan a .vellumapp bundle. +/// Wire type: `"open_bundle"` +public struct OpenBundleMessage: Encodable, Sendable { + public let type: String = "open_bundle" + public let filePath: String + + public init(filePath: String) { + self.filePath = filePath + } +} + +/// Sent to request the list of all past sessions/conversations. +/// Wire type: `"session_list"` +public struct SessionListRequestMessage: Encodable, Sendable { + public let type: String = "session_list" + + public init() {} +} + +/// Sent to request message history for a specific session. +/// Wire type: `"history_request"` +public struct HistoryRequestMessage: Encodable, Sendable { + public let type: String = "history_request" + public let sessionId: String + + public init(sessionId: String) { + self.sessionId = sessionId + } +} + +/// Sent to request the list of available skills. +/// Wire type: `"skills_list"` +public struct SkillsListRequestMessage: Encodable, Sendable { + public let type: String = "skills_list" + + public init() {} +} + +/// Sent to request the full body of a specific skill. +/// Wire type: `"skill_detail"` +public struct SkillDetailRequestMessage: Encodable, Sendable { + public let type: String = "skill_detail" + public let skillId: String + + public init(skillId: String) { + self.skillId = skillId + } +} + +/// Enable a skill. Wire type: "skills_enable" +public struct SkillsEnableMessage: Encodable, Sendable { + public let type: String = "skills_enable" + public let name: String + + public init(name: String) { + self.name = name + } +} + +/// Disable a skill. Wire type: "skills_disable" +public struct SkillsDisableMessage: Encodable, Sendable { + public let type: String = "skills_disable" + public let name: String + + public init(name: String) { + self.name = name + } +} + +/// Configure a skill's env/apiKey/config. Wire type: "skills_configure" +public struct SkillsConfigureMessage: Encodable, Sendable { + public let type: String = "skills_configure" + public let name: String + public let env: [String: String]? + public let apiKey: String? + public let config: [String: AnyCodable]? + + public init(name: String, env: [String: String]? = nil, apiKey: String? = nil, config: [String: AnyCodable]? = nil) { + self.name = name + self.env = env + self.apiKey = apiKey + self.config = config + } +} + +/// Install a skill from ClaWHub. Wire type: "skills_install" +public struct SkillsInstallMessage: Encodable, Sendable { + public let type: String = "skills_install" + public let slug: String + public let version: String? + + public init(slug: String, version: String? = nil) { + self.slug = slug + self.version = version + } +} + +/// Uninstall a skill. Wire type: "skills_uninstall" +public struct SkillsUninstallMessage: Encodable, Sendable { + public let type: String = "skills_uninstall" + public let name: String + + public init(name: String) { + self.name = name + } +} + +/// Update a skill. Wire type: "skills_update" +public struct SkillsUpdateMessage: Encodable, Sendable { + public let type: String = "skills_update" + public let name: String + + public init(name: String) { + self.name = name + } +} + +/// Check for skill updates. Wire type: "skills_check_updates" +public struct SkillsCheckUpdatesMessage: Encodable, Sendable { + public let type: String = "skills_check_updates" + + public init() {} +} + +/// Search for skills on ClaWHub. Wire type: "skills_search" +public struct SkillsSearchMessage: Encodable, Sendable { + public let type: String = "skills_search" + public let query: String + + public init(query: String) { + self.query = query + } +} + +/// Inspect a ClaWHub skill for detailed info. Wire type: "skills_inspect" +public struct SkillsInspectMessage: Encodable, Sendable { + public let type: String = "skills_inspect" + public let slug: String + + public init(slug: String) { + self.slug = slug + } +} + +/// Response to a sign_bundle_payload request from the daemon. +/// Wire type: `"sign_bundle_payload_response"` +public struct SignBundlePayloadResponseMessage: Encodable, Sendable { + public let type: String = "sign_bundle_payload_response" + public let signature: String + public let keyId: String + public let publicKey: String + + public init(signature: String, keyId: String, publicKey: String) { + self.signature = signature + self.keyId = keyId + self.publicKey = publicKey + } +} + +/// Response to a get_signing_identity request from the daemon. +/// Wire type: `"get_signing_identity_response"` +public struct GetSigningIdentityResponseMessage: Encodable, Sendable { + public let type: String = "get_signing_identity_response" + public let keyId: String + public let publicKey: String + + public init(keyId: String, publicKey: String) { + self.keyId = keyId + self.publicKey = publicKey + } +} + +// MARK: - Server → Client Messages (Decodable) + +/// Action to execute from the inference server. +public struct CuActionMessage: Decodable, Sendable { + public let sessionId: String + public let toolName: String + public let input: [String: AnyCodable] + public let reasoning: String? + public let stepNumber: Int + + public init(sessionId: String, toolName: String, input: [String: AnyCodable], reasoning: String?, stepNumber: Int) { + self.sessionId = sessionId + self.toolName = toolName + self.input = input + self.reasoning = reasoning + self.stepNumber = stepNumber + } +} + +/// Session completed successfully. +public struct CuCompleteMessage: Decodable, Sendable { + public let sessionId: String + public let summary: String + public let stepCount: Int + public let isResponse: Bool? + + public init(sessionId: String, summary: String, stepCount: Int, isResponse: Bool?) { + self.sessionId = sessionId + self.summary = summary + self.stepCount = stepCount + self.isResponse = isResponse + } +} + +/// Session-level error from the server. +public struct CuErrorMessage: Decodable, Sendable { + public let sessionId: String + public let message: String + + public init(sessionId: String, message: String) { + self.sessionId = sessionId + self.message = message + } +} + +/// Streamed text delta from the assistant's response. +public struct AssistantTextDeltaMessage: Decodable, Sendable { + public let text: String + public let sessionId: String? + + public init(text: String, sessionId: String? = nil) { + self.text = text + self.sessionId = sessionId + } +} + +/// Streamed thinking delta from the assistant's reasoning. +public struct AssistantThinkingDeltaMessage: Decodable, Sendable { + public let thinking: String + + public init(thinking: String) { + self.thinking = thinking + } +} + +/// Signals that the assistant's message is complete. +public struct MessageCompleteMessage: Decodable, Sendable { + public let sessionId: String? + + public init(sessionId: String? = nil) { + self.sessionId = sessionId + } +} + +/// Session metadata from the server (e.g. generated title). +public struct SessionInfoMessage: Decodable, Sendable { + public let sessionId: String + public let title: String + /// Echoed from the `session_create` request so the caller can match + /// this response to its specific request. + public let correlationId: String? + + public init(sessionId: String, title: String, correlationId: String? = nil) { + self.sessionId = sessionId + self.title = title + self.correlationId = correlationId + } +} + +/// Daemon response after classifying and routing a task_submit. +public struct TaskRoutedMessage: Decodable, Sendable { + public let sessionId: String + public let interactionType: String + /// The task text passed to the escalated session. + public let task: String? + /// Set when a text_qa session escalates to computer_use via request_computer_control. + public let escalatedFrom: String? +} + +/// Result from ambient observation analysis. +public struct AmbientResultMessage: Decodable, Sendable { + public let requestId: String + public let decision: String + public let summary: String? + public let suggestion: String? +} + +/// Surface show command from daemon. +/// Wire type: `"ui_surface_show"` +public struct UiSurfaceShowMessage: Decodable, Sendable { + public let sessionId: String + public let surfaceId: String + public let surfaceType: String + public let title: String? + public let data: AnyCodable + public let actions: [SurfaceActionData]? + /// `"inline"` embeds in chat, `"panel"` shows a floating window. + public let display: String? + + public init(sessionId: String, surfaceId: String, surfaceType: String, title: String?, data: AnyCodable, actions: [SurfaceActionData]?, display: String?) { + self.sessionId = sessionId + self.surfaceId = surfaceId + self.surfaceType = surfaceType + self.title = title + self.data = data + self.actions = actions + self.display = display + } +} + +public struct SurfaceActionData: Decodable, Sendable { + public let id: String + public let label: String + public let style: String? +} + +/// Surface update command from daemon. +/// Wire type: `"ui_surface_update"` +public struct UiSurfaceUpdateMessage: Decodable, Sendable { + public let sessionId: String + public let surfaceId: String + public let data: AnyCodable +} + +/// Surface dismiss command from daemon. +/// Wire type: `"ui_surface_dismiss"` +public struct UiSurfaceDismissMessage: Decodable, Sendable { + public let sessionId: String + public let surfaceId: String +} + +/// Confirms generation was cancelled. +public struct GenerationCancelledMessage: Decodable, Sendable { + public let sessionId: String? + + public init(sessionId: String?) { + self.sessionId = sessionId + } +} + +/// Notifies client that active generation yielded to queued work at a checkpoint. +/// Wire type: `"generation_handoff"` +public struct GenerationHandoffMessage: Decodable, Sendable { + public let sessionId: String + public let requestId: String? + public let queuedCount: Int + + public init(sessionId: String, requestId: String?, queuedCount: Int) { + self.sessionId = sessionId + self.requestId = requestId + self.queuedCount = queuedCount + } +} + +/// Notifies client that a message has been queued for processing. +/// Wire type: `"message_queued"` +public struct MessageQueuedMessage: Decodable, Sendable { + public let sessionId: String + public let requestId: String + public let position: Int + + public init(sessionId: String, requestId: String, position: Int) { + self.sessionId = sessionId + self.requestId = requestId + self.position = position + } +} + +/// Notifies client that a queued message has been dequeued and is now being processed. +/// Wire type: `"message_dequeued"` +public struct MessageDequeuedMessage: Decodable, Sendable { + public let sessionId: String + public let requestId: String + + public init(sessionId: String, requestId: String) { + self.sessionId = sessionId + self.requestId = requestId + } +} + +/// Server-level error message. +public struct ErrorMessage: Decodable, Sendable { + public let message: String + + public init(message: String) { + self.message = message + } +} + +/// Response from the daemon for a persistent app data request. +/// Wire type: `"app_data_response"` +public struct AppDataResponseMessage: Decodable, Sendable { + public let surfaceId: String + public let callId: String + public let success: Bool + public let result: AnyCodable? + public let error: String? +} + +/// ClaWHub metadata for a skill. +public struct ClawhubInfo: Codable, Sendable { + public let author: String + public let stars: Int + public let installs: Int + public let reports: Int + public let publishedAt: String +} + +/// Missing requirements preventing a skill from full operation. +public struct MissingRequirements: Codable, Sendable { + public let bins: [String]? + public let env: [String]? + public let permissions: [String]? +} + +/// Full skill info from the daemon's resolved skill list. +public struct SkillInfo: Codable, Sendable, Identifiable { + public var id: String { name } + public let name: String + public let description: String + public let emoji: String? + public let homepage: String? + public let source: String // "bundled" | "managed" | "workspace" | "clawhub" | "extra" + public let state: String // "enabled" | "disabled" | "available" + public let degraded: Bool + public let missingRequirements: MissingRequirements? + public let installedVersion: String? + public let latestVersion: String? + public let updateAvailable: Bool + public let userInvocable: Bool + public let clawhub: ClawhubInfo? + + public init(name: String, description: String, emoji: String?, homepage: String?, source: String, state: String, degraded: Bool, missingRequirements: MissingRequirements?, installedVersion: String?, latestVersion: String?, updateAvailable: Bool, userInvocable: Bool, clawhub: ClawhubInfo?) { + self.name = name + self.description = description + self.emoji = emoji + self.homepage = homepage + self.source = source + self.state = state + self.degraded = degraded + self.missingRequirements = missingRequirements + self.installedVersion = installedVersion + self.latestVersion = latestVersion + self.updateAvailable = updateAvailable + self.userInvocable = userInvocable + self.clawhub = clawhub + } +} + +/// Backward-compatible alias for code referencing the old name. +public typealias SkillSummaryItem = SkillInfo + +/// Response containing the list of available skills. +/// Wire type: `"skills_list_response"` +public struct SkillsListResponseMessage: Decodable, Sendable { + public let skills: [SkillInfo] +} + +/// Response containing the full body of a specific skill. +/// Wire type: `"skill_detail_response"` +public struct SkillDetailResponseMessage: Decodable, Sendable { + public let skillId: String + public let body: String + public let error: String? +} + +/// Push event: skill state changed. Wire type: "skills_state_changed" +public struct SkillStateChangedMessage: Decodable, Sendable { + public let name: String + public let state: String // "enabled" | "disabled" | "installed" | "uninstalled" +} + +/// Push event: updates available. Wire type: "skills_updates_available" +public struct SkillsUpdatesAvailableMessage: Decodable, Sendable { + public struct UpdateInfo: Decodable, Sendable { + public let name: String + public let installedVersion: String + public let latestVersion: String + } + public let skills: [UpdateInfo] +} + +/// A ClaWHub skill returned from a search or explore query. +public struct ClawhubSkillItem: Decodable, Sendable, Identifiable, Equatable { + public var id: String { slug } + public let name: String + public let slug: String + public let description: String + public let author: String + public let stars: Int + public let installs: Int + public let version: String + /// Epoch milliseconds when the skill was first published. + public let createdAt: Int + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + name = try container.decodeIfPresent(String.self, forKey: .name) ?? "" + slug = try container.decode(String.self, forKey: .slug) + description = try container.decodeIfPresent(String.self, forKey: .description) ?? "" + author = try container.decodeIfPresent(String.self, forKey: .author) ?? "" + stars = try container.decodeIfPresent(Int.self, forKey: .stars) ?? 0 + installs = try container.decodeIfPresent(Int.self, forKey: .installs) ?? 0 + version = try container.decodeIfPresent(String.self, forKey: .version) ?? "" + createdAt = try container.decodeIfPresent(Int.self, forKey: .createdAt) ?? 0 + } + + private enum CodingKeys: String, CodingKey { + case name, slug, description, author, stars, installs, version, createdAt + } +} + +/// Wrapper for ClaWHub search results embedded in `skills_operation_response.data`. +public struct ClawhubSearchData: Decodable, Sendable { + public let skills: [ClawhubSkillItem] +} + +/// Generic operation response. Wire type: "skills_operation_response" +public struct SkillsOperationResponseMessage: Decodable, Sendable { + public let operation: String + public let success: Bool + public let error: String? + public let data: ClawhubSearchData? +} + +/// Skill info from a ClaWHub inspect response. +public struct ClawhubInspectSkill: Decodable, Sendable { + public let slug: String + public let displayName: String + public let summary: String +} + +/// Owner info from a ClaWHub inspect response. +public struct ClawhubInspectOwner: Decodable, Sendable { + public let handle: String + public let displayName: String + public let image: String? +} + +/// Stats from a ClaWHub inspect response. +public struct ClawhubInspectStats: Decodable, Sendable { + public let stars: Int + public let installs: Int + public let downloads: Int + public let versions: Int + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + stars = try container.decodeIfPresent(Int.self, forKey: .stars) ?? 0 + installs = try container.decodeIfPresent(Int.self, forKey: .installs) ?? 0 + downloads = try container.decodeIfPresent(Int.self, forKey: .downloads) ?? 0 + versions = try container.decodeIfPresent(Int.self, forKey: .versions) ?? 0 + } + + private enum CodingKeys: String, CodingKey { + case stars, installs, downloads, versions + } +} + +/// Version info from a ClaWHub inspect response. +public struct ClawhubInspectVersion: Decodable, Sendable { + public let version: String + public let changelog: String? +} + +/// File entry from a ClaWHub inspect response. +public struct ClawhubInspectFile: Decodable, Sendable { + public let path: String + public let size: Int + public let contentType: String? +} + +/// Full inspect data for a ClaWHub skill. +public struct ClawhubInspectData: Decodable, Sendable { + public let skill: ClawhubInspectSkill + public let owner: ClawhubInspectOwner? + public let stats: ClawhubInspectStats? + public let createdAt: Int? + public let updatedAt: Int? + public let latestVersion: ClawhubInspectVersion? + public let files: [ClawhubInspectFile]? + public let skillMdContent: String? +} + +/// Response from inspecting a ClaWHub skill. +/// Wire type: "skills_inspect_response" +public struct SkillsInspectResponseMessage: Decodable, Sendable { + public let slug: String + public let data: ClawhubInspectData? + public let error: String? +} + +/// Response containing the list of past sessions. +/// Wire type: `"session_list_response"` +public struct SessionListResponseMessage: Decodable, Sendable { + public struct SessionItem: Decodable, Sendable { + public let id: String + public let title: String + public let updatedAt: Int + } + public let sessions: [SessionItem] +} + +/// Response containing message history for a session. +/// Wire type: `"history_response"` +public struct HistoryResponseMessage: Decodable, Sendable { + public let sessionId: String + public struct HistoryToolCallItem: Decodable, Sendable { + public let name: String + public let input: [String: AnyCodable] + public let result: String? + public let isError: Bool? + } + public struct HistoryMessageItem: Decodable, Sendable { + public let role: String + public let text: String + public let timestamp: Int + public let toolCalls: [HistoryToolCallItem]? + } + public let messages: [HistoryMessageItem] +} + +/// A single trust rule item returned from the daemon. +public struct TrustRuleItem: Decodable, Sendable, Identifiable { + public let id: String + public let tool: String + public let pattern: String + public let scope: String + public let decision: String + public let priority: Int + public let createdAt: Double +} + +/// Response containing all trust rules. +/// Wire type: `"trust_rules_list_response"` +public struct TrustRulesListResponseMessage: Decodable, Sendable { + public let rules: [TrustRuleItem] +} + +/// A single app item returned from the daemon. +public struct AppItem: Decodable, Sendable, Identifiable { + public let id: String + public let name: String + public let description: String? + public let icon: String? + public let createdAt: Int +} + +/// Response containing the list of all apps. +/// Wire type: `"apps_list_response"` +public struct AppsListResponseMessage: Decodable, Sendable { + public let apps: [AppItem] +} + +/// A single shared app item returned from the daemon. +public struct SharedAppItem: Decodable, Sendable, Identifiable { + public var id: String { uuid } + public let uuid: String + public let name: String + public let description: String? + public let icon: String? + public let entry: String + public let trustTier: String + public let signerDisplayName: String? + public let bundleSizeBytes: Int + public let installedAt: String +} + +/// Response containing the list of shared apps. +/// Wire type: `"shared_apps_list_response"` +public struct SharedAppsListResponseMessage: Decodable, Sendable { + public let apps: [SharedAppItem] +} + +/// Response from deleting a shared app. +/// Wire type: `"shared_app_delete_response"` +public struct SharedAppDeleteResponseMessage: Decodable, Sendable { + public let success: Bool +} + +/// Response from bundling an app. +/// Wire type: `"bundle_app_response"` +public struct BundleAppResponseMessage: Decodable, Sendable { + public let bundlePath: String +} + +/// Request from daemon to sign a bundle payload. +/// Wire type: `"sign_bundle_payload"` +public struct SignBundlePayloadMessage: Decodable, Sendable { + public let payload: String +} + +/// Timer completed notification from daemon. +/// Wire type: `"timer_completed"` +public struct TimerCompletedMessage: Decodable, Sendable { + public let sessionId: String + public let timerId: String + public let label: String + public let durationMinutes: Double +} + +/// Tool execution started. +/// Wire type: `"tool_use_start"` +public struct ToolUseStartMessage: Decodable, Sendable { + public let toolName: String + public let input: [String: AnyCodable] + public let sessionId: String? +} + +/// Streaming tool output chunk. +/// Wire type: `"tool_output_chunk"` +public struct ToolOutputChunkMessage: Decodable, Sendable { + public let chunk: String +} + +/// Tool execution completed. +/// Wire type: `"tool_result"` +public struct ToolResultMessage: Decodable, Sendable { + public let toolName: String + public let result: String + public let isError: Bool? + public let diff: ConfirmationRequestMessage.ConfirmationDiffInfo? + public let status: String? + public let sessionId: String? + /// Base64-encoded image data from tool contentBlocks (e.g. browser_screenshot). + public let imageData: String? +} + +/// Follow-up suggestion response from daemon. +/// Wire type: `"suggestion_response"` +public struct SuggestionResponseMessage: Decodable, Sendable { + public let requestId: String + public let suggestion: String? + public let source: String +} + +/// Secret input request from daemon. +/// Wire type: `"secret_request"` +public struct SecretRequestMessage: Decodable, Sendable { + public let requestId: String + public let service: String + public let field: String + public let label: String + public let description: String? + public let placeholder: String? + public let sessionId: String? +} + +/// Permission confirmation request from daemon. +/// Wire type: `"confirmation_request"` +public struct ConfirmationRequestMessage: Decodable, Sendable { + public let requestId: String + public let toolName: String + public let input: [String: AnyCodable] + public let riskLevel: String + public let allowlistOptions: [ConfirmationAllowlistOption] + public let scopeOptions: [ConfirmationScopeOption] + public let diff: ConfirmationDiffInfo? + public let sandboxed: Bool? + public let sessionId: String? + + public struct ConfirmationAllowlistOption: Decodable, Sendable, Equatable { + public let label: String + public let description: String? + public let pattern: String + + public init(label: String, description: String?, pattern: String) { + self.label = label + self.description = description + self.pattern = pattern + } + } + public struct ConfirmationScopeOption: Decodable, Sendable, Equatable { + public let label: String + public let scope: String + + public init(label: String, scope: String) { + self.label = label + self.scope = scope + } + } + public struct ConfirmationDiffInfo: Decodable, Sendable, Equatable { + public let filePath: String + public let oldContent: String + public let newContent: String + public let isNewFile: Bool + + public init(filePath: String, oldContent: String, newContent: String, isNewFile: Bool) { + self.filePath = filePath + self.oldContent = oldContent + self.newContent = newContent + self.isNewFile = isNewFile + } + } +} + +/// Request a follow-up suggestion for the current session. +/// Wire type: `"suggestion_request"` +public struct SuggestionRequestMessage: Encodable, Sendable { + public let type: String = "suggestion_request" + public let sessionId: String + public let requestId: String + + public init(sessionId: String, requestId: String) { + self.sessionId = sessionId + self.requestId = requestId + } +} + +/// Client response to a permission confirmation request. +/// Wire type: `"confirmation_response"` +public struct ConfirmationResponseMessage: Encodable, Sendable { + public let type: String = "confirmation_response" + public let requestId: String + public let decision: String + public let selectedPattern: String? + public let selectedScope: String? + + public init(requestId: String, decision: String, selectedPattern: String? = nil, selectedScope: String? = nil) { + self.requestId = requestId + self.decision = decision + self.selectedPattern = selectedPattern + self.selectedScope = selectedScope + } +} + +/// Client response to a secret input request. +/// Wire type: `"secret_response"` +public struct SecretResponseMessage: Encodable, Sendable { + public let type: String = "secret_response" + public let requestId: String + public let value: String? + + public init(requestId: String, value: String?) { + self.requestId = requestId + self.value = value + } +} + +/// Sent to add a trust rule (allowlist/denylist) independently of a confirmation response. +/// Wire type: `"add_trust_rule"` +public struct AddTrustRuleMessage: Encodable, Sendable { + public let type: String = "add_trust_rule" + public let toolName: String + public let pattern: String + public let scope: String + public let decision: String + + public init(toolName: String, pattern: String, scope: String, decision: String) { + self.toolName = toolName + self.pattern = pattern + self.scope = scope + self.decision = decision + } +} + +/// Request all trust rules from the daemon. +/// Wire type: `"trust_rules_list"` +public struct TrustRulesListMessage: Encodable, Sendable { + public let type: String = "trust_rules_list" + + public init() {} +} + +/// Remove a trust rule by its ID. +/// Wire type: `"remove_trust_rule"` +public struct RemoveTrustRuleMessage: Encodable, Sendable { + public let type: String = "remove_trust_rule" + public let id: String + + public init(id: String) { + self.id = id + } +} + +/// Update fields on an existing trust rule. +/// Wire type: `"update_trust_rule"` +public struct UpdateTrustRuleMessage: Encodable, Sendable { + public let type: String = "update_trust_rule" + public let id: String + public let tool: String? + public let pattern: String? + public let scope: String? + public let decision: String? + public let priority: Int? + + public init(id: String, tool: String? = nil, pattern: String? = nil, scope: String? = nil, decision: String? = nil, priority: Int? = nil) { + self.id = id + self.tool = tool + self.pattern = pattern + self.scope = scope + self.decision = decision + self.priority = priority + } +} + +/// Response from opening and scanning a .vellumapp bundle. +/// Wire type: `"open_bundle_response"` +public struct OpenBundleResponseMessage: Decodable, Sendable { + public struct Manifest: Decodable, Sendable { + public let formatVersion: Int + public let name: String + public let description: String? + public let icon: String? + public let createdAt: String + public let createdBy: String + public let entry: String + public let capabilities: [String] + + private enum CodingKeys: String, CodingKey { + case formatVersion = "format_version" + case name, description, icon + case createdAt = "created_at" + case createdBy = "created_by" + case entry, capabilities + } + } + public struct ScanResult: Decodable, Sendable { + public let passed: Bool + public let blocked: [String] + public let warnings: [String] + } + public struct SignatureResult: Decodable, Sendable { + public let trustTier: String + public let signerKeyId: String? + public let signerDisplayName: String? + public let signerAccount: String? + } + public let manifest: Manifest + public let scanResult: ScanResult + public let signatureResult: SignatureResult + public let bundleSizeBytes: Int +} + +/// Discriminated union of all server → client message types relevant to the macOS client. +/// Decodes via the `"type"` field in the JSON payload. +public enum ServerMessage: Decodable, Sendable { + case cuAction(CuActionMessage) + case cuComplete(CuCompleteMessage) + case cuError(CuErrorMessage) + case assistantTextDelta(AssistantTextDeltaMessage) + case assistantThinkingDelta(AssistantThinkingDeltaMessage) + case messageComplete(MessageCompleteMessage) + case sessionInfo(SessionInfoMessage) + case sessionListResponse(SessionListResponseMessage) + case historyResponse(HistoryResponseMessage) + case taskRouted(TaskRoutedMessage) + case error(ErrorMessage) + case ambientResult(AmbientResultMessage) + case uiSurfaceShow(UiSurfaceShowMessage) + case uiSurfaceUpdate(UiSurfaceUpdateMessage) + case uiSurfaceDismiss(UiSurfaceDismissMessage) + case generationCancelled(GenerationCancelledMessage) + case generationHandoff(GenerationHandoffMessage) + case confirmationRequest(ConfirmationRequestMessage) + case secretRequest(SecretRequestMessage) + case appDataResponse(AppDataResponseMessage) + case messageQueued(MessageQueuedMessage) + case messageDequeued(MessageDequeuedMessage) + case skillsListResponse(SkillsListResponseMessage) + case skillDetailResponse(SkillDetailResponseMessage) + case skillStateChanged(SkillStateChangedMessage) + case skillsUpdatesAvailable(SkillsUpdatesAvailableMessage) + case skillsOperationResponse(SkillsOperationResponseMessage) + case skillsInspectResponse(SkillsInspectResponseMessage) + case suggestionResponse(SuggestionResponseMessage) + case toolUseStart(ToolUseStartMessage) + case toolOutputChunk(ToolOutputChunkMessage) + case toolResult(ToolResultMessage) + case timerCompleted(TimerCompletedMessage) + case trustRulesListResponse(TrustRulesListResponseMessage) + case appsListResponse(AppsListResponseMessage) + case sharedAppsListResponse(SharedAppsListResponseMessage) + case sharedAppDeleteResponse(SharedAppDeleteResponseMessage) + case bundleAppResponse(BundleAppResponseMessage) + case openBundleResponse(OpenBundleResponseMessage) + case signBundlePayload(SignBundlePayloadMessage) + case getSigningIdentity + case pong + case unknown(String) + + private enum CodingKeys: String, CodingKey { + case type + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + + switch type { + case "cu_action": + let message = try CuActionMessage(from: decoder) + self = .cuAction(message) + case "cu_complete": + let message = try CuCompleteMessage(from: decoder) + self = .cuComplete(message) + case "cu_error": + let message = try CuErrorMessage(from: decoder) + self = .cuError(message) + case "assistant_text_delta": + let message = try AssistantTextDeltaMessage(from: decoder) + self = .assistantTextDelta(message) + case "assistant_thinking_delta": + let message = try AssistantThinkingDeltaMessage(from: decoder) + self = .assistantThinkingDelta(message) + case "message_complete": + let message = try MessageCompleteMessage(from: decoder) + self = .messageComplete(message) + case "session_info": + let message = try SessionInfoMessage(from: decoder) + self = .sessionInfo(message) + case "session_list_response": + let message = try SessionListResponseMessage(from: decoder) + self = .sessionListResponse(message) + case "history_response": + let message = try HistoryResponseMessage(from: decoder) + self = .historyResponse(message) + case "task_routed": + let message = try TaskRoutedMessage(from: decoder) + self = .taskRouted(message) + case "error": + let message = try ErrorMessage(from: decoder) + self = .error(message) + case "ambient_result": + let message = try AmbientResultMessage(from: decoder) + self = .ambientResult(message) + case "ui_surface_show": + let message = try UiSurfaceShowMessage(from: decoder) + self = .uiSurfaceShow(message) + case "ui_surface_update": + let message = try UiSurfaceUpdateMessage(from: decoder) + self = .uiSurfaceUpdate(message) + case "ui_surface_dismiss": + let message = try UiSurfaceDismissMessage(from: decoder) + self = .uiSurfaceDismiss(message) + case "generation_cancelled": + let message = try GenerationCancelledMessage(from: decoder) + self = .generationCancelled(message) + case "generation_handoff": + let message = try GenerationHandoffMessage(from: decoder) + self = .generationHandoff(message) + case "confirmation_request": + let message = try ConfirmationRequestMessage(from: decoder) + self = .confirmationRequest(message) + case "secret_request": + let message = try SecretRequestMessage(from: decoder) + self = .secretRequest(message) + case "app_data_response": + let message = try AppDataResponseMessage(from: decoder) + self = .appDataResponse(message) + case "message_queued": + let message = try MessageQueuedMessage(from: decoder) + self = .messageQueued(message) + case "message_dequeued": + let message = try MessageDequeuedMessage(from: decoder) + self = .messageDequeued(message) + case "skills_list_response": + let message = try SkillsListResponseMessage(from: decoder) + self = .skillsListResponse(message) + case "skill_detail_response": + let message = try SkillDetailResponseMessage(from: decoder) + self = .skillDetailResponse(message) + case "skills_state_changed": + let message = try SkillStateChangedMessage(from: decoder) + self = .skillStateChanged(message) + case "skills_updates_available": + let message = try SkillsUpdatesAvailableMessage(from: decoder) + self = .skillsUpdatesAvailable(message) + case "skills_operation_response": + let message = try SkillsOperationResponseMessage(from: decoder) + self = .skillsOperationResponse(message) + case "skills_inspect_response": + let message = try SkillsInspectResponseMessage(from: decoder) + self = .skillsInspectResponse(message) + case "suggestion_response": + let message = try SuggestionResponseMessage(from: decoder) + self = .suggestionResponse(message) + case "tool_use_start": + let message = try ToolUseStartMessage(from: decoder) + self = .toolUseStart(message) + case "tool_output_chunk": + let message = try ToolOutputChunkMessage(from: decoder) + self = .toolOutputChunk(message) + case "tool_result": + let message = try ToolResultMessage(from: decoder) + self = .toolResult(message) + case "timer_completed": + let message = try TimerCompletedMessage(from: decoder) + self = .timerCompleted(message) + case "trust_rules_list_response": + let message = try TrustRulesListResponseMessage(from: decoder) + self = .trustRulesListResponse(message) + case "apps_list_response": + let message = try AppsListResponseMessage(from: decoder) + self = .appsListResponse(message) + case "shared_apps_list_response": + let message = try SharedAppsListResponseMessage(from: decoder) + self = .sharedAppsListResponse(message) + case "shared_app_delete_response": + let message = try SharedAppDeleteResponseMessage(from: decoder) + self = .sharedAppDeleteResponse(message) + case "bundle_app_response": + let message = try BundleAppResponseMessage(from: decoder) + self = .bundleAppResponse(message) + case "open_bundle_response": + let message = try OpenBundleResponseMessage(from: decoder) + self = .openBundleResponse(message) + case "sign_bundle_payload": + let message = try SignBundlePayloadMessage(from: decoder) + self = .signBundlePayload(message) + case "get_signing_identity": + self = .getSigningIdentity + case "pong": + self = .pong + default: + self = .unknown(type) + } + } +}