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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

## [1.6.20-build-74] - 2026-08-15

### Added

- **MCP permissions and approval** — users can set permissions per tool or for an entire server and review requested MCP calls together before execution
- **Project support options** — Settings now offers optional monthly and annual subscriptions alongside one-time coffee purchases, without locking any features

### Changed

- Agent workflows now allow up to 300 seconds of active processing and 15 rounds for longer multi-tool tasks

### Fixed

- GitHub MCP tools using standard JSON Schema constraints are now available to the model and executable through the approval flow
- Conversation widgets now use more compact typography and balanced header spacing to prevent compressed or crowded layouts

## [1.6.15-build-71] - 2026-08-13

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<img src="https://img.shields.io/badge/License-AGPL%20v3.0-blue?style=flat-square" alt="License" />
<img src="https://img.shields.io/badge/Platform-iOS%2026+%20|%20iPadOS%2026+%20|%20macOS%2026+-blue?style=flat-square" alt="Platform" />
<img src="https://img.shields.io/badge/Swift-6+-orange?style=flat-square&logo=swift" alt="Swift" />
<img src="https://img.shields.io/badge/Version-1.6.15-brightgreen?style=flat-square" alt="Version 1.6.15" />
<img src="https://img.shields.io/badge/Version-1.6.20-brightgreen?style=flat-square" alt="Version 1.6.20" />
</p>

OpenClient connects directly to the AI server you configure, without an OpenClient-hosted proxy or subscription.
Expand Down
13 changes: 8 additions & 5 deletions TestFlight/WhatToTest.en-US.txt
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
Hi there! We've got some great new features for you in this update.

***1.6.15:
***1.6.20:

Major improvements to iCloud conversation syncing, especially when switching between devices or returning to the app.
New section in Settings to manage iCloud data.
General stability, reliability, and performance improvements.
Test the new per-tool MCP permissions and approval flow: set tools to Always Allow, Ask, or Deny, then review requested MCP calls before execution.
You can now support OpenClient with optional monthly or annual subscriptions, alongside one-time coffee purchases. No features are locked.
Fix widgets layout issues.
• Minor bug fixes and improvements for a smoother experience.

***Recent Updates:

• Major improvements to iCloud conversation syncing, especially when switching between devices or returning to the app.
• New section in Settings to manage iCloud data.
• General stability, reliability, and performance improvements.
• You can now generate images directly in chat with supported models.
• We’ve added update notifications so you can easily keep the app up to date.
• The app can now display important announcements and news directly on the Home screen.
Expand All @@ -20,4 +23,4 @@ Hi there! We've got some great new features for you in this update.

Thanks for your continued support and for helping us build the best possible LLM client together.

Remember: you can suggest and vote on new features from the Feedback section in Settings.
Remember: you can suggest and vote on new features from the Feedback section in Settings.
6 changes: 3 additions & 3 deletions Widgets/App/Widgets/ConversationsOverviewWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ private extension ConversationsOverviewWidgetView {
var header: some View {
HStack(alignment: .center) {
Text(String(localized: "Recent"))
.font(.headline)
.font(.subheadline)
.fontWeight(.semibold)
.foregroundStyle(.primary)
Spacer()
HStack(spacing: 5) {
Expand Down Expand Up @@ -126,7 +127,6 @@ private extension ConversationsOverviewWidgetView {
}
}
}
.frame(height: 30)
.padding(.horizontal, 2.5)
}

Expand All @@ -143,7 +143,7 @@ private extension ConversationsOverviewWidgetView {
}

var conversationsList: some View {
let maxItems = family == .systemMedium ? 2 : 6
let maxItems = family == .systemMedium ? 2 : 5
return VStack(spacing: 0) {
ForEach(Array(entry.conversations.prefix(maxItems).enumerated()), id: \.element.id) { index, conversation in
if index > 0 {
Expand Down
7 changes: 4 additions & 3 deletions Widgets/App/Widgets/LatestConversationWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ private extension LatestConversationWidgetView {
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(Color.accentColor)
Text(String(localized: "Continue"))
.font(.headline)
.font(.subheadline)
.fontWeight(.semibold)
.foregroundStyle(.primary)
Spacer()
}
.frame(height: 30)
.padding(.horizontal, 2.5)
}

Expand All @@ -119,7 +119,7 @@ private extension LatestConversationWidgetView {
.foregroundStyle(.tertiary)
}
Text(conversation.title.isEmpty ? String(localized: "New Chat") : conversation.title)
.font(.subheadline)
.font(.caption)
.fontWeight(.semibold)
.foregroundStyle(.primary)
.lineLimit(1)
Expand All @@ -139,6 +139,7 @@ private extension LatestConversationWidgetView {
}
.padding(2.5)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions Widgets/App/Widgets/PinnedConversationsWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,11 @@ private extension PinnedConversationsWidgetView {
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(Color.accentColor)
Text(String(localized: "Pinned"))
.font(.headline)
.font(.subheadline)
.fontWeight(.semibold)
.foregroundStyle(.primary)
Spacer()
}
.frame(height: 30)
.padding(.horizontal, 2.5)
}

Expand All @@ -115,7 +115,7 @@ private extension PinnedConversationsWidgetView {
}

var conversationsList: some View {
let maxItems = family == .systemMedium ? 2 : 6
let maxItems = family == .systemMedium ? 2 : 5
return VStack(spacing: 0) {
ForEach(Array(entry.conversations.prefix(maxItems).enumerated()), id: \.element.id) { index, conversation in
if index > 0 {
Expand Down
6 changes: 3 additions & 3 deletions Widgets/App/Widgets/TaggedConversationsWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@ private extension TaggedConversationsWidgetView {
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(Color.accentColor)
Text(entry.tag)
.font(.headline)
.font(.subheadline)
.fontWeight(.semibold)
.foregroundStyle(.primary)
.lineLimit(1)
Spacer()
}
.frame(height: 30)
.padding(.horizontal, 2.5)
}

Expand All @@ -140,7 +140,7 @@ private extension TaggedConversationsWidgetView {
}

var conversationsList: some View {
let maxItems = family == .systemMedium ? 2 : 6
let maxItems = family == .systemMedium ? 2 : 5
return VStack(spacing: 0) {
ForEach(Array(entry.conversations.prefix(maxItems).enumerated()), id: \.element.id) { index, conversation in
if index > 0 {
Expand Down
2 changes: 1 addition & 1 deletion Widgets/App/Widgets/WidgetConversationRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct WidgetConversationRow: View {
.padding(.top, 4)
VStack(alignment: .leading, spacing: 2) {
Text(conversation.title.isEmpty ? String(localized: "New Chat") : conversation.title)
.font(.subheadline)
.font(.caption)
.fontWeight(.semibold)
.foregroundStyle(.primary)
.lineLimit(1)
Expand Down
14 changes: 13 additions & 1 deletion openclient-llm-macOS/Views/MenuBarChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,29 @@ struct MenuBarChatView: View {
// MARK: - Properties

var onOpenInApp: () -> Void
var onAuthorizationStateChanged: (Bool) -> Void = { _ in }

@State private var chatId = UUID()
@State private var viewModel = ChatViewModel()

// MARK: - View

var body: some View {
VStack(spacing: 0) {
headerBar
Divider()
ChatView()
ChatView(
isMenuBarPresentation: true,
presentsMCPAuthorization: false,
viewModel: viewModel
)
.id(chatId)
}
.frame(width: 380, height: 540)
.mcpToolAuthorizationPresentation(viewModel: viewModel, compact: true)
.onChange(of: viewModel.mcpAuthorizationCoordinator.pendingBatch != nil, initial: true) { _, isPending in
onAuthorizationStateChanged(isPending)
}
}
}

Expand All @@ -50,6 +60,8 @@ private extension MenuBarChatView {
.buttonStyle(.plain)
.foregroundStyle(.secondary)
Button {
viewModel.send(.stopStreamingTapped)
viewModel = ChatViewModel()
chatId = UUID()
} label: {
Image(systemName: "square.and.pencil")
Expand Down
25 changes: 18 additions & 7 deletions openclient-llm-macOS/Views/MenuBarManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ final class MenuBarManager: NSObject {

private var statusItem: NSStatusItem?
private var popover: NSPopover?
private var isAuthorizationPending = false

// MARK: - Public

Expand All @@ -37,13 +38,19 @@ final class MenuBarManager: NSObject {
pop.contentSize = NSSize(width: 380, height: 540)
pop.behavior = .transient
pop.contentViewController = NSHostingController(
rootView: MenuBarChatView { [weak self] in
self?.popover?.performClose(nil)
NSApplication.shared.activate()
NSApplication.shared.windows
.first { !($0 is NSPanel) }?
.makeKeyAndOrderFront(nil)
}
rootView: MenuBarChatView(
onOpenInApp: { [weak self] in
self?.popover?.performClose(nil)
NSApplication.shared.activate()
NSApplication.shared.windows
.first { !($0 is NSPanel) }?
.makeKeyAndOrderFront(nil)
},
onAuthorizationStateChanged: { [weak self, weak pop] isPending in
self?.isAuthorizationPending = isPending
pop?.behavior = isPending ? .applicationDefined : .transient
}
)
)

statusItem = item
Expand All @@ -55,6 +62,10 @@ final class MenuBarManager: NSObject {
@objc private func togglePopover(_ sender: AnyObject?) {
guard let popover, let button = statusItem?.button else { return }
if popover.isShown {
guard !isAuthorizationPending else {
NSSound.beep()
return
}
popover.performClose(sender)
} else {
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
Expand Down
16 changes: 16 additions & 0 deletions openclient-llm-test/Core/Managers/KeychainManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,35 @@ final class KeychainManagerTests: XCTestCase {
XCTAssertEqual(sut.getAPIKey(), "sk-new-key")
}

func test_setServerConfiguration_validValues_storesURLAndAPIKeyTogether() {
// Given
let serverURL = "https://example.com"
let apiKey = "sk-test-key"

// When
let didSave = sut.setServerConfiguration(serverBaseURL: serverURL, apiKey: apiKey)

// Then
XCTAssertTrue(didSave)
XCTAssertEqual(sut.getServerBaseURL(), serverURL)
XCTAssertEqual(sut.getAPIKey(), apiKey)
}

// MARK: - Tests — Delete All

func test_deleteAll_removesAllStoredValues() {
// Given
sut.setServerBaseURL("https://example.com")
sut.setAPIKey("sk-test-key")
XCTAssertTrue(sut.setMCPAuthorizationScope("scope"))

// When
sut.deleteAll()

// Then
XCTAssertEqual(sut.getServerBaseURL(), "")
XCTAssertEqual(sut.getAPIKey(), "")
XCTAssertNil(sut.getMCPAuthorizationScope())
}

// MARK: - Tests — Isolation
Expand Down
14 changes: 7 additions & 7 deletions openclient-llm-test/Core/Managers/RemoteConfigManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ final class RemoteConfigManagerTests: XCTestCase {
// Given
let manager = RemoteConfigManager(
endpoint: endpoint,
dataLoader: try makeLoader(version: "1.6.15"),
dataLoader: try makeLoader(version: "1.6.20"),
defaults: defaults,
refreshInterval: 6 * 60 * 60
)
Expand All @@ -49,13 +49,13 @@ final class RemoteConfigManagerTests: XCTestCase {
let config = try await manager.loadConfig()

// Then
XCTAssertEqual(config.appUpdate.ios.latestVersion, "1.6.15")
XCTAssertEqual(config.appUpdate.ios.latestVersion, "1.6.20")
}

func test_loadConfig_withFreshCache_returnsCachedConfig() async throws {
// Given
let now = Date(timeIntervalSince1970: 1_000)
_ = try await makeManager(version: "1.6.15", now: now, refreshInterval: .zero).loadConfig()
_ = try await makeManager(version: "1.6.20", now: now, refreshInterval: .zero).loadConfig()
let manager = try makeManager(
version: "2.0.0",
now: now.addingTimeInterval(60),
Expand All @@ -66,13 +66,13 @@ final class RemoteConfigManagerTests: XCTestCase {
let config = try await manager.loadConfig()

// Then
XCTAssertEqual(config.appUpdate.ios.latestVersion, "1.6.15")
XCTAssertEqual(config.appUpdate.ios.latestVersion, "1.6.20")
}

func test_loadConfig_withExpiredCache_downloadsLatestConfig() async throws {
// Given
let now = Date(timeIntervalSince1970: 1_000)
_ = try await makeManager(version: "1.6.15", now: now, refreshInterval: .zero).loadConfig()
_ = try await makeManager(version: "1.6.20", now: now, refreshInterval: .zero).loadConfig()
let manager = try makeManager(
version: "2.0.0",
now: now.addingTimeInterval((6 * 60 * 60) + 1),
Expand All @@ -89,7 +89,7 @@ final class RemoteConfigManagerTests: XCTestCase {
func test_loadConfig_whenRefreshFails_returnsCachedConfig() async throws {
// Given
let now = Date(timeIntervalSince1970: 1_000)
_ = try await makeManager(version: "1.6.15", now: now, refreshInterval: .zero).loadConfig()
_ = try await makeManager(version: "1.6.20", now: now, refreshInterval: .zero).loadConfig()
let manager = RemoteConfigManager(
endpoint: endpoint,
dataLoader: { _ in throw URLError(.notConnectedToInternet) },
Expand All @@ -102,7 +102,7 @@ final class RemoteConfigManagerTests: XCTestCase {
let config = try await manager.loadConfig()

// Then
XCTAssertEqual(config.appUpdate.ios.latestVersion, "1.6.15")
XCTAssertEqual(config.appUpdate.ios.latestVersion, "1.6.20")
}
}

Expand Down
Loading