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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ 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.40-build-91] - [Unreleased]

### Added

- Ten new themed app icons for Halloween and Christmas on iOS and iPadOS

## [1.6.35-build-90] - 2026-08-28

### 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.35-brightgreen?style=flat-square" alt="Version 1.6.35" />
<img src="https://img.shields.io/badge/Version-1.6.40-brightgreen?style=flat-square" alt="Version 1.6.40" />
</p>

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

***1.6.35:
***1.6.40:

• 10 new themed app icons for Halloween and Christmas.
• Minor bug fixes and improvements for a smoother experience.

***Recent Updates:

• Choose from 20 app icons in Settings on iPhone and iPad.
• Long chats now preserve earlier context before the next model response, with safer handling when requests are cancelled or interrupted.
• Scroll through long conversations and verify that scrolling remains smooth while the floating date indicator updates.
• While a model is responding, scroll through the conversation and verify that the visible history stays stable, then catches up when scrolling stops.
• Start a model response, leave OpenClient, and verify that its Live Activity shows progress through reasoning, tool use, image generation, and saving.
• Minor bug fixes and improvements for a smoother experience.

***Recent Updates:

• Fixed an issue that could cause the app to freeze when manually scrolling while a model was responding, or after several responses.
• On macOS, add OpenClient widgets to the desktop or Notification Center and test New Chat, Search, Quick Actions, Continue Chat, Recent, Pinned, and Tagged Conversations.
• On macOS, add the New Chat control and verify that it opens the app directly in a new conversation.
Expand Down
72 changes: 63 additions & 9 deletions openclient-llm-test/Core/Managers/RemoteConfigManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,41 @@ final class RemoteConfigManagerTests: XCTestCase {
// Then
XCTAssertTrue(config.isTipJarEnabled)
}

func test_loadConfig_showIconPacksConfigured_returnsIdentifiers() async throws {
// Given
let manager = RemoteConfigManager(
endpoint: endpoint,
dataLoader: try makeLoader(
version: "1.0.0",
showIconPacks: ["colors", "christmas"]
),
defaults: defaults,
refreshInterval: .zero
)

// When
let config = try await manager.loadConfig()

// Then
XCTAssertEqual(config.showIconPacks, ["colors", "christmas"])
}

func test_loadConfig_showIconPacksMissing_returnsNil() async throws {
// Given
let manager = RemoteConfigManager(
endpoint: endpoint,
dataLoader: try makeLoader(version: "1.0.0", showIconPacks: nil),
defaults: defaults,
refreshInterval: .zero
)

// When
let config = try await manager.loadConfig()

// Then
XCTAssertNil(config.showIconPacks)
}
}

// MARK: - Private
Expand All @@ -159,26 +194,36 @@ private extension RemoteConfigManagerTests {

func makeLoader(
version: String,
tipJarEnabled: Bool? = true
tipJarEnabled: Bool? = true,
showIconPacks: [String]? = nil
) throws -> RemoteConfigDataLoader {
let endpoint = try XCTUnwrap(endpoint)
let response = try XCTUnwrap(
HTTPURLResponse(url: endpoint, statusCode: 200, httpVersion: nil, headerFields: nil)
)
let data = Data(configJSON(version: version, tipJarEnabled: tipJarEnabled).utf8)
let data = Data(
configJSON(
version: version,
tipJarEnabled: tipJarEnabled,
showIconPacks: showIconPacks
).utf8
)
return { _ in (data, response) }
}

func configJSON(version: String, tipJarEnabled: Bool?) -> String {
let tipJar = tipJarEnabled.map {
"""
"settings_section": { "tip_option": \($0) },
"""
} ?? ""
func configJSON(
version: String,
tipJarEnabled: Bool?,
showIconPacks: [String]?
) -> String {
let settingsSection = settingsSectionJSON(
tipJarEnabled: tipJarEnabled,
showIconPacks: showIconPacks
)
return """
{
"schema_version": 1,
\(tipJar)
\(settingsSection)
"maintenance_mode": { "enabled": false },
"app_update": {
"ios": {
Expand Down Expand Up @@ -212,4 +257,13 @@ private extension RemoteConfigManagerTests {
}
"""
}

func settingsSectionJSON(tipJarEnabled: Bool?, showIconPacks: [String]?) -> String {
guard tipJarEnabled != nil || showIconPacks != nil else { return "" }
let packValues = showIconPacks?.map { "\"\($0)\"" }.joined(separator: ", ")
let packs = packValues.map { ", \"show_icon_packs\": [\($0)]" } ?? ""
return """
"settings_section": { "tip_option": \(tipJarEnabled ?? true)\(packs) },
"""
}
}
32 changes: 29 additions & 3 deletions openclient-llm-test/Core/Models/AppIconTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@ import XCTest

@MainActor
final class AppIconTests: XCTestCase {
func test_allCases_containsTwentyIcons() {
func test_allCases_containsThirtyIcons() {
// When
let icons = AppIcon.allCases

// Then
XCTAssertEqual(icons.count, 20)
XCTAssertEqual(icons.count, 30)
}

func test_icons_eachCategory_containsFiveIcons() {
// When
let categoryCounts = AppIcon.Category.allCases.map { AppIcon.icons(in: $0).count }

// Then
XCTAssertEqual(categoryCounts, [5, 5, 5, 5])
XCTAssertEqual(categoryCounts, [5, 5, 5, 5, 5, 5])
}

func test_alternateIconName_defaultIcon_returnsNil() {
Expand All @@ -44,6 +44,32 @@ final class AppIconTests: XCTestCase {
XCTAssertEqual(icon, .defaultIcon)
}

func test_visibleCategories_missingConfiguration_returnsAllCategories() {
// When
let categories = AppIcon.Category.visibleCategories(showIconPacks: nil)

// Then
XCTAssertEqual(categories, AppIcon.Category.allCases)
}

func test_visibleCategories_configured_returnsOpenClientAndKnownPacks() {
// When
let categories = AppIcon.Category.visibleCategories(
showIconPacks: ["christmas", "colors", "unknown"]
)

// Then
XCTAssertEqual(categories, [.openClient, .colors, .christmas])
}

func test_visibleCategories_emptyConfiguration_returnsOpenClient() {
// When
let categories = AppIcon.Category.visibleCategories(showIconPacks: [])

// Then
XCTAssertEqual(categories, [.openClient])
}

func test_previewImage_allIcons_loadFromMainBundle() {
// When
let missingIcons = AppIcon.allCases.filter { icon in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,36 @@ extension SettingsViewModelTests {
}
XCTAssertTrue(loadedState.isTipJarEnabled)
}

func test_send_viewAppeared_iconPacksConfigured_setsVisibleCategories() {
// Given
mockRemoteConfigManager.currentConfig = .stub(
showIconPacks: ["special", "halloween", "unknown"]
)

// When
sut.send(.viewAppeared)

// Then
guard case .loaded(let loadedState) = sut.state else {
XCTFail("Expected loaded state")
return
}
XCTAssertEqual(loadedState.visibleAppIconCategories, [.openClient, .special, .halloween])
}

func test_send_viewAppeared_iconPacksMissing_setsAllCategoriesVisible() {
// Given
mockRemoteConfigManager.currentConfig = .stub(showIconPacks: nil)

// When
sut.send(.viewAppeared)

// Then
guard case .loaded(let loadedState) = sut.state else {
XCTFail("Expected loaded state")
return
}
XCTAssertEqual(loadedState.visibleAppIconCategories, AppIcon.Category.allCases)
}
}
19 changes: 17 additions & 2 deletions openclient-llm-test/Mocks/MockRemoteConfigManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ extension RemoteConfig {
isForceUpdate: Bool = false,
latestVersion: String = "1.0.0",
banner: Banner? = nil,
isTipJarEnabled: Bool? = true
isTipJarEnabled: Bool? = true,
showIconPacks: [String]? = nil
) -> RemoteConfig {
let update = PlatformUpdate(
enabled: isUpdateEnabled,
Expand All @@ -41,7 +42,21 @@ extension RemoteConfig {
maintenanceMode: .init(enabled: isMaintenanceEnabled),
appUpdate: .init(ios: update, macos: update),
banner: banner ?? .init(active: false, dismissBannerKey: "test", platforms: [], items: [:]),
settingsSection: isTipJarEnabled.map { .init(tipOption: $0) }
settingsSection: makeSettingsSection(
isTipJarEnabled: isTipJarEnabled,
showIconPacks: showIconPacks
)
)
}

private static func makeSettingsSection(
isTipJarEnabled: Bool?,
showIconPacks: [String]?
) -> SettingsSection? {
guard isTipJarEnabled != nil || showIconPacks != nil else { return nil }
return SettingsSection(
tipOption: isTipJarEnabled ?? true,
showIconPacks: showIconPacks
)
}
}
24 changes: 12 additions & 12 deletions openclient-llm.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.6.35;
MARKETING_VERSION = 1.6.40;
PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm.macos-widgets";
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand Down Expand Up @@ -837,7 +837,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.6.35;
MARKETING_VERSION = 1.6.40;
PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm.macos-widgets";
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand Down Expand Up @@ -984,7 +984,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 8089F4102F7D000500465D6C /* Secrets.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "Arctic Aurora Berry Blueprint Copper Holo Honeydew Lavender Mars Midnight Mono Ocean Retro Sakura Solar Sunset Terminal Ultraviolet Violet";
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "Arctic Aurora Berry BloodMoon Blueprint CandyCane Copper GhostNight GoldenNoel HauntedSlime Holo Honeydew Lavender Mars Midnight Mono NorthPole Ocean PumpkinNight Retro Sakura SantaGlow SilentNight Solar Sunset Terminal Ultraviolet Violet WitchingHour";
ASSETCATALOG_COMPILER_APPICON_NAME = Default;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "openclient-llm/Resources/openclient-llm.entitlements";
Expand Down Expand Up @@ -1013,7 +1013,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.6.35;
MARKETING_VERSION = 1.6.40;
PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
Expand All @@ -1033,7 +1033,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 8089F4102F7D000500465D6C /* Secrets.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "Arctic Aurora Berry Blueprint Copper Holo Honeydew Lavender Mars Midnight Mono Ocean Retro Sakura Solar Sunset Terminal Ultraviolet Violet";
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "Arctic Aurora Berry BloodMoon Blueprint CandyCane Copper GhostNight GoldenNoel HauntedSlime Holo Honeydew Lavender Mars Midnight Mono NorthPole Ocean PumpkinNight Retro Sakura SantaGlow SilentNight Solar Sunset Terminal Ultraviolet Violet WitchingHour";
ASSETCATALOG_COMPILER_APPICON_NAME = Default;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "openclient-llm/Resources/openclient-llm.entitlements";
Expand Down Expand Up @@ -1062,7 +1062,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.6.35;
MARKETING_VERSION = 1.6.40;
PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
Expand Down Expand Up @@ -1120,7 +1120,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.6.35;
MARKETING_VERSION = 1.6.40;
PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm";
PRODUCT_NAME = OpenClient;
REGISTER_APP_GROUPS = YES;
Expand Down Expand Up @@ -1176,7 +1176,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.6.35;
MARKETING_VERSION = 1.6.40;
PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm";
PRODUCT_NAME = OpenClient;
REGISTER_APP_GROUPS = YES;
Expand Down Expand Up @@ -1254,7 +1254,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.6.35;
MARKETING_VERSION = 1.6.40;
PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand Down Expand Up @@ -1285,7 +1285,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.6.35;
MARKETING_VERSION = 1.6.40;
PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand Down Expand Up @@ -1318,7 +1318,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.6.35;
MARKETING_VERSION = 1.6.40;
PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm.widgets";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand Down Expand Up @@ -1351,7 +1351,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.6.35;
MARKETING_VERSION = 1.6.40;
PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm.widgets";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand Down
Loading