Skip to content
Open
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
4 changes: 2 additions & 2 deletions apps/swift-ios/Core/T3Client+ReviewGuide.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,12 @@ public struct PullRequestReviewGuideResult: Codable, Equatable, Sendable {

extension T3Client {
// Decode only this setting so older hosts remain gated without changing shared settings models.
private struct ReviewGuideSettings: Decodable {
private struct ReviewGuideSettings: Decodable, Sendable {
let reviewGuidesEnabled: Bool?
}

public func reviewGuidesEnabled() async throws -> Bool {
let settings = try await rpc.request("server.getSettings", as: ReviewGuideSettings.self)
let settings = try await serverSettings(as: ReviewGuideSettings.self)
return settings.reviewGuidesEnabled == true
}

Expand Down
17 changes: 15 additions & 2 deletions apps/swift-ios/Core/T3Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,20 @@ public actor T3Client {

/// Read before replacing an override entry, including changes from other clients.
public func serverSettings() async throws -> ServerSettingsSnapshot {
try await rpc.request("server.getSettings", as: ServerSettingsSnapshot.self)
try await serverSettings(as: ServerSettingsSnapshot.self)
}

private struct SettingsConfig<Settings: Decodable & Sendable>: Decodable, Sendable {
let settings: Settings
}

func serverSettings<Settings: Decodable & Sendable>(as type: Settings.Type) async throws -> Settings {
// Bypass the subscription cache before read-modify-write operations.
let config = try await rpc.request(
RPCMethod.serverGetConfig.rawValue,
as: SettingsConfig<Settings>.self
)
return config.settings
}

public func updateSettings(_ change: ServerSettingsChange) async throws
Expand Down Expand Up @@ -300,7 +313,7 @@ public actor T3Client {
}

public func setProviderEnabled(instanceID: String, driver: String, enabled: Bool) async throws {
let settings = try await rpc.request("server.getSettings", as: JSONValue.self)
let settings = try await serverSettings(as: JSONValue.self)
let patch = ProviderSettingsPatch.enabled(settings: settings, instanceID: instanceID, driver: driver, enabled: enabled)
let _: JSONValue = try await rpc.request("server.updateSettings", payload: .object(["patch": patch]), as: JSONValue.self)
}
Expand Down
61 changes: 60 additions & 1 deletion apps/swift-ios/Tests/CoreTests/T3ClientServerConfigTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,59 @@ import XCTest

@MainActor
final class T3ClientServerConfigTests: XCTestCase {
func testSettingsReadsStayFreshWhenTheLegacyMethodIsUnavailable() async throws {
let connection = ServerConfigTestConnection(mode: .snapshot)
let client = makeClient(connection: connection)
let cached = try await client.serverConfig()
XCTAssertEqual(cached.settings?.newWorktreesStartFromOrigin, false)

await connection.replaceSettings(.object([
"newWorktreesStartFromOrigin": .bool(true),
"favorites": .array([.object(["provider": .string("codex"), "model": .string("gpt-5")])]),
]))
let settings = try await client.serverSettings()
XCTAssertTrue(settings.newWorktreesStartFromOrigin)
XCTAssertEqual(settings.favorites?.first?.model, "gpt-5")
let tags = await connection.tags()
XCTAssertFalse(tags.contains("server.getSettings"))
await client.disconnect()
}

func testProviderToggleAndReviewGuidesReadSettingsFromConfiguration() async throws {
let connection = ServerConfigTestConnection(mode: .snapshot)
let client = makeClient(connection: connection)
_ = try await client.serverConfig()
await connection.replaceSettings(.object([
"reviewGuidesEnabled": .bool(true),
"providerInstances": .object([
"codex": .object([
"driver": .string("codex"), "enabled": .bool(true),
"config": .object(["binaryPath": .string("/custom/codex"), "futureOption": .bool(true)]),
]),
"other": .object(["driver": .string("claude"), "enabled": .bool(true)]),
]),
]))
try await client.setProviderEnabled(instanceID: "codex", driver: "codex", enabled: false)
let writes = await connection.payloads(for: "server.updateSettings")
let instances = try XCTUnwrap(writes.first?["patch"]?["providerInstances"])
XCTAssertEqual(instances["codex"]?["enabled"], .bool(false))
XCTAssertEqual(instances["codex"]?["config"]?["binaryPath"], .string("/custom/codex"))
XCTAssertEqual(instances["codex"]?["config"]?["futureOption"], .bool(true))
XCTAssertEqual(instances["other"]?["enabled"], .bool(true))

let enabled = try await client.reviewGuidesEnabled()
XCTAssertTrue(enabled)
await connection.replaceSettings(.object(["reviewGuidesEnabled": .bool(false)]))
let disabled = try await client.reviewGuidesEnabled()
XCTAssertFalse(disabled)
await connection.replaceSettings(.object([:]))
let unsupported = try await client.reviewGuidesEnabled()
XCTAssertFalse(unsupported)
let tags = await connection.tags()
XCTAssertFalse(tags.contains("server.getSettings"))
await client.disconnect()
}

func testQuestionAttachmentsRequireTheServerCapabilityBeforeUploading() async throws {
let connection = ServerConfigTestConnection(mode: .snapshot)
let client = makeClient(connection: connection)
Expand Down Expand Up @@ -464,6 +517,7 @@ private actor ServerConfigTestConnection: WebSocketConnection {
private let failResetCreditSend: Bool
private let supportsQuestionAttachments: Bool?
private var requestTags: [String] = []
private var settingsOverride: JSONValue?
private var requestPayloads: [String: [JSONValue]] = [:]
private var subscriptionRequestID: Int?
private var refreshRequestID: Int?
Expand Down Expand Up @@ -500,6 +554,10 @@ private actor ServerConfigTestConnection: WebSocketConnection {
}
case "server.getConfig":
try enqueue(success(id: id, value: config(id: "codex-old")))
case "server.getSettings":
try enqueue(failure(id: id, message: "Unknown request tag: server.getSettings"))
case "server.updateSettings":
try enqueue(success(id: id, value: settingsOverride ?? .object([:])))
case "server.refreshProviders":
refreshRequestID = id
case "attachments.createUploadUrl":
Expand Down Expand Up @@ -530,6 +588,7 @@ private actor ServerConfigTestConnection: WebSocketConnection {
}

func tags() -> [String] { requestTags }
func replaceSettings(_ settings: JSONValue) { settingsOverride = settings }
func payloads(for tag: String) -> [JSONValue] { requestPayloads[tag] ?? [] }

func waitForRequestCount(_ count: Int) async {
Expand Down Expand Up @@ -592,7 +651,7 @@ private actor ServerConfigTestConnection: WebSocketConnection {
}
return .object([
"providers": .array([provider(id: id)]),
"settings": .object([
"settings": settingsOverride ?? .object([
"defaultThreadEnvMode": .string("worktree"),
"newWorktreesStartFromOrigin": .bool(false),
]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ private actor DeliveryWebSocketConnection: WebSocketConnection {
let request = try JSONDecoder.t3.decode(JSONValue.self, from: data)
guard let id = request["id"], let tag = request["tag"]?.stringValue else { return }
let config: JSONValue = .object([
"settings": .object([:]),
"providers": .array([.object([
"instanceId": .string("codex"), "driver": .string("codex"),
"enabled": .bool(true), "installed": .bool(true), "status": .string("ready"),
Expand All @@ -463,8 +464,6 @@ private actor DeliveryWebSocketConnection: WebSocketConnection {
value = .object(["sequence": .number(101)])
case RPCMethod.serverGetConfig.rawValue:
value = config
case "server.getSettings":
value = .object([:])
case RPCMethod.subscribeServerConfig.rawValue:
try enqueue(.object([
"_tag": .string("Chunk"), "requestId": id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2149,7 +2149,7 @@ private actor MultiEnvironmentHTTPTransport: HTTPTransport {
ModelSelection(instanceId: "codex", model: "gpt-5.6-sol"),
ModelSelection(instanceId: "claudeAgent", model: "claude-opus-4-1"),
]
return .object(["providers": .array(selections.map { selection in .object([
return .object(["settings": .object([:]), "providers": .array(selections.map { selection in .object([
"instanceId": .string(selection.instanceId),
"driver": .string(selection.instanceId),
"enabled": .bool(true), "installed": .bool(true), "status": .string("ready"),
Expand Down Expand Up @@ -2314,9 +2314,9 @@ private actor MultiEnvironmentFixtureSocket: WebSocketConnection {
case RPCMethod.subscribeServerConfig.rawValue:
response = .object(["_tag": .string("Chunk"), "requestId": id,
"values": .array([.object(["type": .string("snapshot"), "config": await transport.serverConfig(host: host)])])])
case "server.getSettings":
case RPCMethod.serverGetConfig.rawValue:
response = .object(["_tag": .string("Exit"), "requestId": id,
"exit": .object(["_tag": .string("Success"), "value": .object([:])])])
"exit": .object(["_tag": .string("Success"), "value": await transport.serverConfig(host: host)])])
case RPCMethod.subscribeShell.rawValue where interruptShellSubscriptions:
// Let startup load configuration, then break live shell delivery.
throw URLError(.networkConnectionLost)
Expand Down Expand Up @@ -2448,8 +2448,8 @@ private actor MultiEnvironmentConfigurationServer {
case "projects.ensureScratch":
scratchRequestHosts.append(host)
value = .object(["projectId": .string("scratch")])
case "server.getSettings":
value = .object(settingsByHost[host] ?? [:])
case RPCMethod.serverGetConfig.rawValue:
value = config(host: host)
case RPCMethod.subscribeVCSStatus.rawValue:
guard let cwd = request["payload"]?["cwd"]?.stringValue else {
throw URLError(.badServerResponse)
Expand Down
12 changes: 5 additions & 7 deletions apps/swift-ios/Tests/FeatureTests/NativeRetryIdentityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -453,8 +453,7 @@ private actor ConcurrentBootstrapWebSocketConnection: WebSocketConnection {
connectionWaiters.removeAll()
}
if request["tag"]?.stringValue == RPCMethod.serverGetConfig.rawValue
|| request["tag"]?.stringValue == RPCMethod.subscribeServerConfig.rawValue
|| request["tag"]?.stringValue == "server.getSettings",
|| request["tag"]?.stringValue == RPCMethod.subscribeServerConfig.rawValue,
let response = try retryConfigResponse(for: request) {
enqueue(response)
return
Expand Down Expand Up @@ -760,8 +759,7 @@ private actor AmbiguousDispatchWebSocketConnection: WebSocketConnection {
connectionWaiters.removeAll()
}
if request["tag"]?.stringValue == RPCMethod.serverGetConfig.rawValue
|| request["tag"]?.stringValue == RPCMethod.subscribeServerConfig.rawValue
|| request["tag"]?.stringValue == "server.getSettings",
|| request["tag"]?.stringValue == RPCMethod.subscribeServerConfig.rawValue,
let response = try retryConfigResponse(for: request) {
enqueue(response)
return
Expand Down Expand Up @@ -825,8 +823,7 @@ private actor PartialBootstrapWebSocketConnection: WebSocketConnection {
guard [RPCMethod.dispatchCommand.rawValue, "orchestration.launchThread"].contains(request["tag"]?.stringValue ?? ""),
let payload = request["payload"] else {
if request["tag"]?.stringValue == RPCMethod.serverGetConfig.rawValue
|| request["tag"]?.stringValue == RPCMethod.subscribeServerConfig.rawValue
|| request["tag"]?.stringValue == "server.getSettings",
|| request["tag"]?.stringValue == RPCMethod.subscribeServerConfig.rawValue,
let response = try retryConfigResponse(for: request) {
enqueue(response)
}
Expand Down Expand Up @@ -885,7 +882,8 @@ private actor PartialBootstrapWebSocketConnection: WebSocketConnection {

private func retryConfigResponse(for request: JSONValue) throws -> Data? {
guard case let .number(requestID)? = request["id"] else { return nil }
let config: JSONValue = request["tag"]?.stringValue == "server.getSettings" ? .object([:]) : .object([
let config: JSONValue = .object([
"settings": .object([:]),
"providers": .array([.object([
"instanceId": .string("codex"), "driver": .string("codex"),
"enabled": .bool(true), "installed": .bool(true), "status": .string("ready"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ private actor WorkspacePermissionServer: HTTPTransport {
}
let value: JSONValue
switch method {
case "server.getSettings": value = .object([:])
case RPCMethod.serverGetConfig.rawValue: value = .object(["settings": .object([:])])
case "git.preparePullRequestThread":
value = .object(["branch": .string("pr-1"), "worktreePath": .string("/work/pr-1"),
"isOnPullRequestHead": .bool(true)])
Expand Down
Loading