Skip to content

Commit 1f06b89

Browse files
authored
feat(ios): add model picker favorites and recents with working iOS model switching (#100774)
1 parent 8ce620f commit 1f06b89

9 files changed

Lines changed: 475 additions & 36 deletions

File tree

apps/.i18n/native-source.json

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20315,151 +20315,191 @@
2031520315
},
2031620316
{
2031720317
"kind": "ui-call",
20318-
"line": 192,
20318+
"line": 195,
2031920319
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2032020320
"source": "Thinking",
2032120321
"surface": "apple",
2032220322
"id": "native.apple.510c6dfa05924ae2"
2032320323
},
2032420324
{
2032520325
"kind": "ui-call",
20326-
"line": 211,
20326+
"line": 216,
2032720327
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2032820328
"source": "Model",
2032920329
"surface": "apple",
2033020330
"id": "native.apple.0fce9d743a640218"
2033120331
},
2033220332
{
2033320333
"kind": "ui-call",
20334-
"line": 234,
20334+
"line": 233,
20335+
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
20336+
"source": "Pinned",
20337+
"surface": "apple",
20338+
"id": "native.apple.745b004e0b5b6b80"
20339+
},
20340+
{
20341+
"kind": "ui-call",
20342+
"line": 241,
20343+
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
20344+
"source": "Recent",
20345+
"surface": "apple",
20346+
"id": "native.apple.69381a0a462ccd84"
20347+
},
20348+
{
20349+
"kind": "ui-call",
20350+
"line": 249,
20351+
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
20352+
"source": "Models",
20353+
"surface": "apple",
20354+
"id": "native.apple.aa321cf4820426ef"
20355+
},
20356+
{
20357+
"kind": "conditional-branch",
20358+
"line": 278,
20359+
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
20360+
"source": "Pin model",
20361+
"surface": "apple",
20362+
"id": "native.apple.75550b03f06d6abc"
20363+
},
20364+
{
20365+
"kind": "conditional-branch",
20366+
"line": 278,
20367+
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
20368+
"source": "Unpin model",
20369+
"surface": "apple",
20370+
"id": "native.apple.af4df27bdbacb5de"
20371+
},
20372+
{
20373+
"kind": "ui-call",
20374+
"line": 282,
2033520375
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2033620376
"source": "Session",
2033720377
"surface": "apple",
2033820378
"id": "native.apple.5dfc0700d8dff8de"
2033920379
},
2034020380
{
2034120381
"kind": "ui-modifier",
20342-
"line": 298,
20382+
"line": 346,
2034320383
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2034420384
"source": "Add Image",
2034520385
"surface": "apple",
2034620386
"id": "native.apple.284b2b94c272591d"
2034720387
},
2034820388
{
2034920389
"kind": "ui-modifier",
20350-
"line": 299,
20390+
"line": 347,
2035120391
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2035220392
"source": "Attachments",
2035320393
"surface": "apple",
2035420394
"id": "native.apple.1963dfa3285cb715"
2035520395
},
2035620396
{
2035720397
"kind": "conditional-branch",
20358-
"line": 469,
20398+
"line": 517,
2035920399
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2036020400
"source": "Talk",
2036120401
"surface": "apple",
2036220402
"id": "native.apple.ee2c0d22f5861159"
2036320403
},
2036420404
{
2036520405
"kind": "conditional-branch",
20366-
"line": 515,
20406+
"line": 563,
2036720407
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2036820408
"source": "Start realtime chat",
2036920409
"surface": "apple",
2037020410
"id": "native.apple.5d5041aefe6dfddd"
2037120411
},
2037220412
{
2037320413
"kind": "conditional-branch",
20374-
"line": 515,
20414+
"line": 563,
2037520415
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2037620416
"source": "Stop realtime chat",
2037720417
"surface": "apple",
2037820418
"id": "native.apple.21a7d3a3386a2218"
2037920419
},
2038020420
{
2038120421
"kind": "conditional-branch",
20382-
"line": 548,
20422+
"line": 596,
2038320423
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2038420424
"source": "Start",
2038520425
"surface": "apple",
2038620426
"id": "native.apple.88c2de261ddae511"
2038720427
},
2038820428
{
2038920429
"kind": "conditional-branch",
20390-
"line": 548,
20430+
"line": 596,
2039120431
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2039220432
"source": "Stop",
2039320433
"surface": "apple",
2039420434
"id": "native.apple.72e5aa4446eaca9e"
2039520435
},
2039620436
{
2039720437
"kind": "ui-call",
20398-
"line": 658,
20438+
"line": 706,
2039920439
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2040020440
"source": "Loading commands",
2040120441
"surface": "apple",
2040220442
"id": "native.apple.9d4bdedcbf74d86c"
2040320443
},
2040420444
{
2040520445
"kind": "ui-call",
20406-
"line": 667,
20446+
"line": 715,
2040720447
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2040820448
"source": "Commands unavailable",
2040920449
"surface": "apple",
2041020450
"id": "native.apple.fdd34aa94629c962"
2041120451
},
2041220452
{
2041320453
"kind": "ui-call",
20414-
"line": 676,
20454+
"line": 724,
2041520455
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2041620456
"source": "Retry",
2041720457
"surface": "apple",
2041820458
"id": "native.apple.a1953e3e9be8b8d5"
2041920459
},
2042020460
{
2042120461
"kind": "ui-call",
20422-
"line": 685,
20462+
"line": 733,
2042320463
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2042420464
"source": "No matching commands",
2042520465
"surface": "apple",
2042620466
"id": "native.apple.0f942ca442f88936"
2042720467
},
2042820468
{
2042920469
"kind": "ui-modifier",
20430-
"line": 817,
20470+
"line": 865,
2043120471
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2043220472
"source": "Stop response",
2043320473
"surface": "apple",
2043420474
"id": "native.apple.afd8a2b9e3bf6d60"
2043520475
},
2043620476
{
2043720477
"kind": "ui-modifier",
20438-
"line": 842,
20478+
"line": 890,
2043920479
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2044020480
"source": "Send message",
2044120481
"surface": "apple",
2044220482
"id": "native.apple.d30969d467c66537"
2044320483
},
2044420484
{
2044520485
"kind": "ui-modifier",
20446-
"line": 857,
20486+
"line": 905,
2044720487
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2044820488
"source": "Refresh",
2044920489
"surface": "apple",
2045020490
"id": "native.apple.f4473f79bb87b2fd"
2045120491
},
2045220492
{
2045320493
"kind": "conditional-branch",
20454-
"line": 965,
20494+
"line": 1013,
2045520495
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2045620496
"source": "Connecting...",
2045720497
"surface": "apple",
2045820498
"id": "native.apple.6c984c0e3ed3a69e"
2045920499
},
2046020500
{
2046120501
"kind": "conditional-branch",
20462-
"line": 965,
20502+
"line": 1013,
2046320503
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift",
2046420504
"source": "Gateway connected",
2046520505
"surface": "apple",

apps/ios/Sources/Chat/IOSGatewayChatTransport.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,40 @@ struct IOSGatewayChatTransport: OpenClawChatTransport {
3636
var agentId: String?
3737
}
3838

39+
private struct SessionPatchModelParams: Encodable {
40+
var key: String
41+
var agentId: String?
42+
var model: String?
43+
44+
enum CodingKeys: String, CodingKey {
45+
case key
46+
case agentId
47+
case model
48+
}
49+
50+
func encode(to encoder: Encoder) throws {
51+
var container = encoder.container(keyedBy: CodingKeys.self)
52+
try container.encode(self.key, forKey: .key)
53+
try container.encodeIfPresent(self.agentId, forKey: .agentId)
54+
if let model {
55+
try container.encode(model, forKey: .model)
56+
} else {
57+
try container.encodeNil(forKey: .model)
58+
}
59+
}
60+
}
61+
62+
private struct ModelsListResponse: Decodable {
63+
var models: [Model]
64+
65+
struct Model: Decodable {
66+
var id: String
67+
var name: String
68+
var provider: String
69+
var contextWindow: Int?
70+
}
71+
}
72+
3973
private struct ChatSendParams: Codable {
4074
var sessionKey: String
4175
var agentId: String?
@@ -139,6 +173,18 @@ struct IOSGatewayChatTransport: OpenClawChatTransport {
139173
completed: self.isAgentWaitCompletionStatus(status))
140174
}
141175

176+
static func decodeModelChoices(_ data: Data) throws -> [OpenClawChatModelChoice] {
177+
let decoded = try JSONDecoder().decode(ModelsListResponse.self, from: data)
178+
return decoded.models.map { model in
179+
let name = model.name.trimmingCharacters(in: .whitespacesAndNewlines)
180+
return OpenClawChatModelChoice(
181+
modelID: model.id,
182+
name: name.isEmpty ? model.id : model.name,
183+
provider: model.provider,
184+
contextWindow: model.contextWindow)
185+
}
186+
}
187+
142188
static func makeCreateSessionParamsJSON(
143189
key: String,
144190
agentId: String? = nil,
@@ -167,6 +213,14 @@ struct IOSGatewayChatTransport: OpenClawChatTransport {
167213
try self.encodeParams(SessionKeyParams(key: sessionKey, agentId: agentId))
168214
}
169215

216+
static func makeSessionPatchModelParamsJSON(
217+
sessionKey: String,
218+
agentId: String? = nil,
219+
model: String?) throws -> String
220+
{
221+
try self.encodeParams(SessionPatchModelParams(key: sessionKey, agentId: agentId, model: model))
222+
}
223+
170224
private static func makeHistoryParamsJSON(sessionKey: String, agentId: String?) throws -> String {
171225
struct Params: Codable {
172226
var sessionKey: String
@@ -226,6 +280,22 @@ struct IOSGatewayChatTransport: OpenClawChatTransport {
226280
return try JSONDecoder().decode(OpenClawChatSessionsListResponse.self, from: res)
227281
}
228282

283+
func listModels() async throws -> [OpenClawChatModelChoice] {
284+
let response = try await self.gateway.request(
285+
method: "models.list",
286+
paramsJSON: nil,
287+
timeoutSeconds: 15)
288+
return try Self.decodeModelChoices(response)
289+
}
290+
291+
func setSessionModel(sessionKey: String, model: String?) async throws {
292+
let json = try Self.makeSessionPatchModelParamsJSON(
293+
sessionKey: sessionKey,
294+
agentId: self.selectedGlobalAgentId(for: sessionKey),
295+
model: model)
296+
_ = try await self.gateway.request(method: "sessions.patch", paramsJSON: json, timeoutSeconds: 15)
297+
}
298+
229299
func setActiveSessionKey(_ sessionKey: String) async throws {
230300
struct Params: Codable {
231301
var key: String

apps/ios/Tests/IOSGatewayChatTransportTests.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,43 @@ struct IOSGatewayChatTransportTests {
4646
#expect(params["limit"] as? Int == 12)
4747
}
4848

49+
@Test func `session model patch params include model and selected agent`() throws {
50+
let params = try self.object(
51+
from: IOSGatewayChatTransport.makeSessionPatchModelParamsJSON(
52+
sessionKey: "global",
53+
agentId: "reviewer",
54+
model: "anthropic/claude-opus-4"))
55+
#expect(params["key"] as? String == "global")
56+
#expect(params["agentId"] as? String == "reviewer")
57+
#expect(params["model"] as? String == "anthropic/claude-opus-4")
58+
}
59+
60+
@Test func `session model patch params encode default model as null`() throws {
61+
let params = try self.object(
62+
from: IOSGatewayChatTransport.makeSessionPatchModelParamsJSON(
63+
sessionKey: "agent:main:main",
64+
model: nil))
65+
#expect(params["key"] as? String == "agent:main:main")
66+
#expect(params["agentId"] == nil)
67+
#expect(params["model"] is NSNull)
68+
}
69+
70+
@Test func `models list response decodes choices and falls back blank names`() throws {
71+
let data = Data(
72+
#"{"models":[{"id":"claude-opus-4","name":"Claude Opus 4","provider":"anthropic","contextWindow":200000,"reasoning":true},{"id":"gpt-5","name":" ","provider":"openai","extra":"ignored"}]}"#.utf8)
73+
let choices = try IOSGatewayChatTransport.decodeModelChoices(data)
74+
75+
#expect(choices.count == 2)
76+
#expect(choices[0].modelID == "claude-opus-4")
77+
#expect(choices[0].name == "Claude Opus 4")
78+
#expect(choices[0].provider == "anthropic")
79+
#expect(choices[0].contextWindow == 200_000)
80+
#expect(choices[1].modelID == "gpt-5")
81+
#expect(choices[1].name == "gpt-5")
82+
#expect(choices[1].provider == "openai")
83+
#expect(choices[1].contextWindow == nil)
84+
}
85+
4986
@Test func `commands list params request text scope with args`() throws {
5087
let params = try self.object(from: IOSGatewayChatTransport.makeCommandsListParamsJSON())
5188
#expect(params["scope"] as? String == "text")

0 commit comments

Comments
 (0)