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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ 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.35-build-86] - 2026-08-26

### Added

- App icon selector in Settings with 20 designs on iOS and iPadOS

### Fixed

- Chat context usage now retains LiteLLM's reported prompt-token count and estimates only the newly generated response, falling back to local estimation when usage is unavailable

## [1.6.30-build-85] - 2026-08-23

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

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

***1.6.30:
***1.6.35:

• 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.
• Choose from 20 app icons in Settings on iPhone and iPad.
• 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.
• Chat messages now show their time, with a floating date indicator when scrolling through conversation history.
• 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.
Expand Down
57 changes: 57 additions & 0 deletions openclient-llm-test/Core/Models/AppIconTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// AppIconTests.swift
// openclient-llm-test
//
// Created by Arturo Carretero Calvo on 26/08/2026.
// Copyright © 2026 Arturo Carretero Calvo. All rights reserved.
//

import UIKit
import XCTest
@testable import openclient_llm

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

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

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

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

func test_alternateIconName_defaultIcon_returnsNil() {
// When
let alternateIconName = AppIcon.defaultIcon.alternateIconName

// Then
XCTAssertNil(alternateIconName)
}

func test_init_unknownAlternateIconName_returnsDefaultIcon() {
// When
let icon = AppIcon(alternateIconName: "Unknown")

// Then
XCTAssertEqual(icon, .defaultIcon)
}

func test_previewImage_allIcons_loadFromMainBundle() {
// When
let missingIcons = AppIcon.allCases.filter { icon in
guard let url = Bundle.main.url(forResource: icon.rawValue, withExtension: "png") else { return true }
return UIImage(contentsOfFile: url.path) == nil
}

// Then
XCTAssertTrue(missingIcons.isEmpty, "Missing icon previews: \(missingIcons.map(\.rawValue))")
}
}
18 changes: 15 additions & 3 deletions openclient-llm-test/Core/Tips/AppTipsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,25 @@ import XCTest
final class AppTipsTests: XCTestCase {
func test_allTips_containsEveryFeatureTip() {
// Given
let expectedCount = 10
let expectedIdentifiers = Set([
AppTips.modelSelector.id,
AppTips.chatAttachments.id,
AppTips.messageActions.id,
AppTips.webSearch.id,
AppTips.chatOptions.id,
AppTips.privateChat.id,
AppTips.contextUsage.id,
AppTips.memory.id,
AppTips.conversationOrganization.id,
AppTips.mcpServers.id,
AppTips.appIconSelection.id
])

// When
let tips = AppTips.allTips
let identifiers = Set(AppTips.allTips.map(\.id))

// Then
XCTAssertEqual(tips.count, expectedCount)
XCTAssertEqual(identifiers, expectedIdentifiers)
}

func test_allTips_haveUniqueIdentifiers() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,44 @@ final class AgentStreamUseCaseContextTests: XCTestCase {

// When
var usages: [TokenUsage] = []
var promptUsages: [Int?] = []
for try await event in sut.execute(
messages: [ChatMessage(role: .user, content: "Time")],
model: "test",
parameters: .default,
toolRegistry: ToolRegistry(tools: [GetCurrentDatetimeTool()])
) {
if case .usage(let usage) = event { usages.append(usage) }
if case .promptUsage(let promptTokens) = event { promptUsages.append(promptTokens) }
}

// Then
XCTAssertEqual(usages.last, TokenUsage(promptTokens: 30, completionTokens: 5, totalTokens: 35))
XCTAssertEqual(promptUsages, [10, 20])
}

func test_execute_finalResponseWithoutUsage_emitsMissingPromptUsage() async throws {
// Given
let firstUsage = TokenUsage(promptTokens: 10, completionTokens: 2, totalTokens: 12)
let first = response(makeToolCallResponse(makeToolCall(id: "call_1")), usage: firstUsage)
let repository = RecordingAgentRepository(responses: [first, makeStopResponse(content: "Done")])
let sut = AgentStreamUseCase(repository: repository)

// When
var promptUsages: [Int?] = []
for try await event in sut.execute(
messages: [ChatMessage(role: .user, content: "Time")],
model: "test",
parameters: .default,
toolRegistry: ToolRegistry(tools: [GetCurrentDatetimeTool()])
) {
if case .promptUsage(let promptTokens) = event { promptUsages.append(promptTokens) }
}

// Then
XCTAssertEqual(promptUsages.count, 2)
XCTAssertEqual(promptUsages[0], 10)
XCTAssertNil(promptUsages[1])
}

func test_execute_timeout_finishesEvenWhenRequestIsStillRunning() async throws {
Expand Down
50 changes: 50 additions & 0 deletions openclient-llm-test/Features/Chat/ChatMessageTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// ChatMessageTests.swift
// openclient-llm-test
//
// Created by Arturo Carretero Calvo on 26/08/2026.
// Copyright © 2026 Arturo Carretero Calvo. All rights reserved.
//

import XCTest
@testable import openclient_llm

@MainActor
final class ChatMessageTests: XCTestCase {
func test_hasSameRequestContent_withNormalizedAttachment_returnsTrue() {
// Given
let messageId = UUID()
let attachmentId = UUID()
let transient = ChatMessage(
id: messageId,
role: .user,
content: "Review this",
attachments: [.init(
id: attachmentId,
type: .pdf,
fileName: "document.pdf",
mimeType: "application/pdf",
fileRelativePath: "",
transientData: Data("PDF".utf8)
)]
)
let persisted = ChatMessage(
id: messageId,
role: .user,
content: "Review this",
attachments: [.init(
id: attachmentId,
type: .pdf,
fileName: "document.pdf",
mimeType: "application/pdf",
fileRelativePath: "Attachments/conversation/document.pdf"
)]
)

// When
let result = transient.hasSameRequestContent(as: persisted)

// Then
XCTAssertTrue(result)
}
}
Loading