Skip to content

Commit b6946a0

Browse files
committed
chore: release 5.0.1
1 parent 469b919 commit b6946a0

69 files changed

Lines changed: 34981 additions & 393737 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## [5.0.1] - 2026-02-26
2+
3+
### Updated
4+
* Finalized Setapp AI API, providing a stable interface for integrating AI capabilities into your apps.
5+
16
## [5.0.0] - 2026-02-16
27

38
### New

Samples/AIViewModel.swift

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import Setapp
2+
import SetappAI
3+
import SwiftUI
4+
import Combine
5+
6+
// MARK: - Message Model
7+
8+
struct ChatMessage: Identifiable, Equatable, Codable {
9+
let id: String
10+
let role: SetappAIAPI.Responses.Role
11+
let content: String
12+
let timestamp: Date
13+
14+
init(id: String = UUID().uuidString, role: SetappAIAPI.Responses.Role, content: String, timestamp: Date = Date()) {
15+
self.id = id
16+
self.role = role
17+
self.content = content
18+
self.timestamp = timestamp
19+
}
20+
}
21+
22+
// MARK: - Conversation State
23+
24+
struct ConversationState: Codable {
25+
var lastResponseId: String?
26+
var selectedModelId: String?
27+
28+
init(lastResponseId: String? = nil, selectedModelId: String? = nil) {
29+
self.lastResponseId = lastResponseId
30+
self.selectedModelId = selectedModelId
31+
}
32+
}
33+
34+
// MARK: - View Model
35+
36+
@Observable
37+
@MainActor
38+
class AIViewModel {
39+
#if os(iOS)
40+
var isActivated: Bool = false
41+
#endif
42+
43+
// Models
44+
var availableModels: [SetappAIAPI.Model] = []
45+
var isLoadingModels = false
46+
var modelsError: Error?
47+
48+
// Conversation state
49+
var state = ConversationState()
50+
var messages: [ChatMessage] = []
51+
var prompt: String = ""
52+
var isStreaming = false
53+
var streamingError: Error?
54+
55+
private var streamingTask: Task<Void, Never>?
56+
private var subscriptionCancellable: AnyCancellable?
57+
private var currentStreamingContent: String = ""
58+
59+
private static let conversationStateKey = "AIViewModel.conversationState"
60+
61+
init() {
62+
#if os(iOS)
63+
subscriptionCancellable = SetappManager.shared.publisher(for: \.subscription)
64+
.sink { [weak self] subscription in
65+
self?.isActivated = subscription?.isActive ?? false
66+
}
67+
#endif
68+
69+
self.loadConversationState()
70+
}
71+
72+
var selectedModel: SetappAIAPI.Model? {
73+
availableModels.first { $0.id == state.selectedModelId }
74+
}
75+
76+
var canSendMessage: Bool {
77+
!isStreaming && !prompt.isEmpty && selectedModel != nil
78+
}
79+
80+
// MARK: - Models API
81+
82+
func loadModels() {
83+
isLoadingModels = true
84+
modelsError = nil
85+
86+
Task {
87+
do {
88+
let models = try await SetappManager.shared.ai.models.list()
89+
self.availableModels = models
90+
91+
// Auto-select first model if none selected
92+
if self.state.selectedModelId == nil, let firstModel = models.first {
93+
self.state.selectedModelId = firstModel.id
94+
self.saveConversationState()
95+
}
96+
97+
self.isLoadingModels = false
98+
} catch {
99+
self.modelsError = error
100+
self.isLoadingModels = false
101+
}
102+
}
103+
}
104+
105+
// MARK: - Conversation Management
106+
107+
func clearConversation() {
108+
state = ConversationState(selectedModelId: state.selectedModelId)
109+
prompt = ""
110+
streamingError = nil
111+
messages = []
112+
saveConversationState()
113+
}
114+
115+
// MARK: - Persistence
116+
117+
private func saveConversationState() {
118+
if let encoded = try? JSONEncoder().encode(state) {
119+
UserDefaults.standard.set(encoded, forKey: Self.conversationStateKey)
120+
}
121+
}
122+
123+
private func loadConversationState() {
124+
guard let data = UserDefaults.standard.data(forKey: Self.conversationStateKey),
125+
let decoded = try? JSONDecoder().decode(ConversationState.self, from: data) else {
126+
return
127+
}
128+
state = decoded
129+
}
130+
131+
// MARK: - Streaming API
132+
133+
func sendMessage() {
134+
guard let model = selectedModel, !prompt.isEmpty else { return }
135+
136+
// Cancel any existing stream
137+
cancelStreaming()
138+
139+
// Add user message
140+
let userMessage = ChatMessage(role: .user, content: prompt)
141+
messages.append(userMessage)
142+
143+
// Clear prompt
144+
let messageContent = prompt
145+
prompt = ""
146+
147+
isStreaming = true
148+
streamingError = nil
149+
currentStreamingContent = ""
150+
151+
streamingTask = Task {
152+
do {
153+
let stream = try await SetappManager.shared.ai.responses.createStream(
154+
model: model,
155+
input: [.message(messageContent)],
156+
previousResponseID: self.state.lastResponseId, // Use for keeping conversation context
157+
include: [.messageOutputTextLogprobs],
158+
store: true,
159+
temperature: 0.5,
160+
streamOptions: .init(includeObfuscation: false)
161+
)
162+
163+
let assistantMessageId = UUID().uuidString
164+
let assistantMessage = ChatMessage(id: assistantMessageId, role: .assistant, content: "")
165+
self.messages.append(assistantMessage)
166+
167+
// Process streaming events
168+
for try await event in stream {
169+
guard !Task.isCancelled else {
170+
self.updateLastMessage(content: self.currentStreamingContent + "\n\n⚠️ Stream cancelled")
171+
break
172+
}
173+
174+
// Handle text deltas
175+
if case let .response(.outputText(.delta(delta))) = event {
176+
self.currentStreamingContent += delta.delta
177+
self.updateLastMessage(content: self.currentStreamingContent)
178+
}
179+
180+
if case let .response(.responseLifecycle(.completed(lifecycleEvent))) = event {
181+
state.lastResponseId = lifecycleEvent.response.id
182+
}
183+
}
184+
185+
if !Task.isCancelled {
186+
// Finalize message
187+
self.updateLastMessage(content: self.currentStreamingContent)
188+
}
189+
190+
self.currentStreamingContent = ""
191+
self.isStreaming = false
192+
self.saveConversationState()
193+
194+
} catch is CancellationError {
195+
self.updateLastMessage(content: self.currentStreamingContent + "\n\n⚠️ Stream cancelled")
196+
self.currentStreamingContent = ""
197+
self.isStreaming = false
198+
self.saveConversationState()
199+
} catch {
200+
self.streamingError = error
201+
self.updateLastMessage(content: self.currentStreamingContent + "\n\n❌ Error: \(error.localizedDescription)")
202+
self.currentStreamingContent = ""
203+
self.isStreaming = false
204+
self.saveConversationState()
205+
}
206+
}
207+
}
208+
209+
func cancelStreaming() {
210+
streamingTask?.cancel()
211+
streamingTask = nil
212+
}
213+
214+
private func updateLastMessage(content: String) {
215+
guard let lastIndex = messages.indices.last else { return }
216+
let lastMessage = messages[lastIndex]
217+
messages[lastIndex] = ChatMessage(
218+
id: lastMessage.id,
219+
role: lastMessage.role,
220+
content: content,
221+
timestamp: lastMessage.timestamp
222+
)
223+
}
224+
}

Samples/Electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"node-gyp": "^10.2.0"
2020
},
2121
"build": {
22-
"buildVersion": "31",
22+
"buildVersion": "32",
2323
"appId": "com.setapp.fmwk.macos.TestApp-setapp",
2424
"productName": "Setapp Electron App Demo",
2525
"copyright": "Copyright 2020-2023 Setapp Limited.",

Samples/SetappAI/App.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import SwiftUI
2+
import Setapp
3+
import SetappAI
4+
5+
@main
6+
struct SetappAISampleApp: App {
7+
8+
init() {
9+
SetappManager.logLevel = .verbose
10+
11+
#if os(iOS)
12+
let configuration = SetappConfiguration(
13+
publicKeyBundle: .main,
14+
publicKeyFilename: "setappPublicKey-iOS.pem"
15+
)
16+
configuration.appGroupIdentifier = "group.setapp"
17+
SetappManager.shared.start(with: configuration)
18+
#endif
19+
20+
// auth client that is created in your vendor account
21+
#if os(iOS)
22+
SetappManager.shared.ai.set(configuration: .init(
23+
authConfiguration: AuthConfiguration(
24+
oauthClientId: "ad82ea588b3a5899a82cafbbbadf6fde451d7d3bacac5dad",
25+
oauthSecret: "6PjJYWPTkMB1xU09XQq47QyP8rIltFTM0FZOSsZB"
26+
)
27+
))
28+
#else
29+
SetappManager.shared.ai.set(configuration: .init(
30+
authConfiguration: AuthConfiguration(
31+
oauthClientId: "28d128d2179763663a93bf6bc7232dd643a436094562023d",
32+
oauthSecret: "Eafev2YDhtXHjjLa5dgPYDSNRmdORUQ1nS72DXmo"
33+
)
34+
))
35+
#endif
36+
}
37+
38+
var body: some Scene {
39+
WindowGroup {
40+
NavigationStack {
41+
ContentView()
42+
}
43+
#if os(iOS)
44+
.onOpenURL { url in
45+
if SetappManager.shared.canOpen(url: url) {
46+
let _ = SetappManager.shared.open(url: url, options: [:])
47+
}
48+
}
49+
#endif
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)