Skip to content

Commit f6e51ff

Browse files
feat(ios): refresh pro UI and gateway flows (#87367)
Summary: - Replace the legacy iOS shell with Pro Command, Chat, Agents, and Settings tabs. - Wire iOS chat/session/settings/diagnostics and realtime Talk flows through gateway-backed APIs. - Add gateway/session and shared chat coverage for the new iOS flow. Verification: - git diff --check - node scripts/run-vitest.mjs src/gateway/server.sessions.create.test.ts src/gateway/talk-realtime-relay.test.ts - swift test --filter ChatViewModelTests (apps/shared/OpenClawKit) - xcodebuild build for Nimrod's iPhone succeeded; install succeeded; launch was blocked because the phone was locked Known follow-up: - Preserve traceLevel in sessions.create parent runtime inheritance and keep the changelog credit in the follow-up patch.
1 parent 65d47dc commit f6e51ff

86 files changed

Lines changed: 12005 additions & 4245 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"images": [
3+
{
4+
"filename": "openclaw-icon.png",
5+
"idiom": "universal"
6+
}
7+
],
8+
"info": {
9+
"author": "xcode",
10+
"version": 1
11+
}
12+
}
5.78 KB
Loading

apps/ios/Sources/Chat/ChatSheet.swift

Lines changed: 0 additions & 47 deletions
This file was deleted.

apps/ios/Sources/Chat/IOSGatewayChatTransport.swift

Lines changed: 180 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,162 @@ import OSLog
66

77
struct IOSGatewayChatTransport: OpenClawChatTransport {
88
private static let logger = Logger(subsystem: "ai.openclaw", category: "ios.chat.transport")
9+
static let defaultChatSendTimeoutMs = 30000
910
private let gateway: GatewayNodeSession
1011

12+
private struct CreateSessionParams: Codable {
13+
var key: String
14+
var label: String?
15+
var parentSessionKey: String?
16+
}
17+
18+
private struct RunParams: Codable {
19+
var sessionKey: String
20+
var runId: String
21+
}
22+
23+
private struct ListSessionsParams: Codable {
24+
var includeGlobal: Bool
25+
var includeUnknown: Bool
26+
var limit: Int?
27+
}
28+
29+
private struct SessionKeyParams: Codable {
30+
var key: String
31+
}
32+
33+
private struct ChatSendParams: Codable {
34+
var sessionKey: String
35+
var message: String
36+
var thinking: String
37+
var attachments: [OpenClawChatAttachmentPayload]?
38+
var timeoutMs: Int
39+
var idempotencyKey: String
40+
}
41+
42+
private struct AgentWaitParams: Codable {
43+
var runId: String
44+
var timeoutMs: Int
45+
}
46+
47+
private struct AgentWaitResponse: Codable {
48+
var runId: String?
49+
var status: String?
50+
var error: String?
51+
}
52+
53+
struct AgentWaitCompletion: Equatable {
54+
var runId: String
55+
var status: String
56+
var completed: Bool
57+
}
58+
59+
static func isAgentWaitCompletionStatus(_ status: String) -> Bool {
60+
switch status.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
61+
case "ok", "completed", "success", "succeeded":
62+
true
63+
default:
64+
false
65+
}
66+
}
67+
1168
init(gateway: GatewayNodeSession) {
1269
self.gateway = gateway
1370
}
1471

15-
func abortRun(sessionKey: String, runId: String) async throws {
16-
struct Params: Codable {
17-
var sessionKey: String
18-
var runId: String
72+
static func agentWaitRequestTimeoutSeconds(timeoutMs: Int) -> Int {
73+
max(1, Int(ceil(Double(timeoutMs) / 1000.0)) + 5)
74+
}
75+
76+
static func makeListSessionsParamsJSON(limit: Int?) throws -> String {
77+
try self.encodeParams(ListSessionsParams(includeGlobal: true, includeUnknown: false, limit: limit))
78+
}
79+
80+
static func makeChatSendParamsJSON(
81+
sessionKey: String,
82+
message: String,
83+
thinking: String,
84+
idempotencyKey: String,
85+
attachments: [OpenClawChatAttachmentPayload]) throws -> String
86+
{
87+
let params = ChatSendParams(
88+
sessionKey: sessionKey,
89+
message: message,
90+
thinking: thinking,
91+
attachments: attachments.isEmpty ? nil : attachments,
92+
timeoutMs: self.defaultChatSendTimeoutMs,
93+
idempotencyKey: idempotencyKey)
94+
return try self.encodeParams(params)
95+
}
96+
97+
static func decodeAgentWaitCompletion(_ data: Data, fallbackRunId: String) throws -> AgentWaitCompletion {
98+
let decoded = try JSONDecoder().decode(AgentWaitResponse.self, from: data)
99+
let status = (decoded.status ?? "unknown").lowercased()
100+
return AgentWaitCompletion(
101+
runId: decoded.runId ?? fallbackRunId,
102+
status: status,
103+
completed: self.isAgentWaitCompletionStatus(status))
104+
}
105+
106+
private static func makeCreateSessionParamsJSON(
107+
key: String,
108+
label: String?,
109+
parentSessionKey: String?) throws -> String
110+
{
111+
let params = CreateSessionParams(
112+
key: key,
113+
label: label,
114+
parentSessionKey: parentSessionKey)
115+
return try self.encodeParams(params)
116+
}
117+
118+
private static func makeRunParamsJSON(sessionKey: String, runId: String) throws -> String {
119+
try self.encodeParams(RunParams(sessionKey: sessionKey, runId: runId))
120+
}
121+
122+
private static func makeSessionKeyParamsJSON(_ sessionKey: String) throws -> String {
123+
try self.encodeParams(SessionKeyParams(key: sessionKey))
124+
}
125+
126+
private static func makeHistoryParamsJSON(sessionKey: String) throws -> String {
127+
struct Params: Codable { var sessionKey: String }
128+
return try self.encodeParams(Params(sessionKey: sessionKey))
129+
}
130+
131+
private static func makeAgentWaitParamsJSON(runId: String, timeoutMs: Int) throws -> String {
132+
try self.encodeParams(AgentWaitParams(runId: runId, timeoutMs: timeoutMs))
133+
}
134+
135+
private static func encodeParams(_ params: some Encodable) throws -> String {
136+
let data = try JSONEncoder().encode(params)
137+
guard let json = String(bytes: data, encoding: .utf8) else {
138+
throw EncodingError.invalidValue(
139+
params,
140+
EncodingError.Context(codingPath: [], debugDescription: "Encoded gateway params were not UTF-8"))
19141
}
20-
let data = try JSONEncoder().encode(Params(sessionKey: sessionKey, runId: runId))
21-
let json = String(data: data, encoding: .utf8)
142+
return json
143+
}
144+
145+
func createSession(
146+
key: String,
147+
label: String?,
148+
parentSessionKey: String?) async throws -> OpenClawChatCreateSessionResponse
149+
{
150+
let json = try Self.makeCreateSessionParamsJSON(
151+
key: key,
152+
label: label,
153+
parentSessionKey: parentSessionKey)
154+
let res = try await self.gateway.request(method: "sessions.create", paramsJSON: json, timeoutSeconds: 15)
155+
return try JSONDecoder().decode(OpenClawChatCreateSessionResponse.self, from: res)
156+
}
157+
158+
func abortRun(sessionKey: String, runId: String) async throws {
159+
let json = try Self.makeRunParamsJSON(sessionKey: sessionKey, runId: runId)
22160
_ = try await self.gateway.request(method: "chat.abort", paramsJSON: json, timeoutSeconds: 10)
23161
}
24162

25163
func listSessions(limit: Int?) async throws -> OpenClawChatSessionsListResponse {
26-
struct Params: Codable {
27-
var includeGlobal: Bool
28-
var includeUnknown: Bool
29-
var limit: Int?
30-
}
31-
let data = try JSONEncoder().encode(Params(includeGlobal: true, includeUnknown: false, limit: limit))
32-
let json = String(data: data, encoding: .utf8)
164+
let json = try Self.makeListSessionsParamsJSON(limit: limit)
33165
let res = try await self.gateway.request(method: "sessions.list", paramsJSON: json, timeoutSeconds: 15)
34166
return try JSONDecoder().decode(OpenClawChatSessionsListResponse.self, from: res)
35167
}
@@ -40,23 +172,17 @@ struct IOSGatewayChatTransport: OpenClawChatTransport {
40172
}
41173

42174
func resetSession(sessionKey: String) async throws {
43-
struct Params: Codable { var key: String }
44-
let data = try JSONEncoder().encode(Params(key: sessionKey))
45-
let json = String(data: data, encoding: .utf8)
175+
let json = try Self.makeSessionKeyParamsJSON(sessionKey)
46176
_ = try await self.gateway.request(method: "sessions.reset", paramsJSON: json, timeoutSeconds: 10)
47177
}
48178

49179
func compactSession(sessionKey: String) async throws {
50-
struct Params: Codable { var key: String }
51-
let data = try JSONEncoder().encode(Params(key: sessionKey))
52-
let json = String(data: data, encoding: .utf8)
180+
let json = try Self.makeSessionKeyParamsJSON(sessionKey)
53181
_ = try await self.gateway.request(method: "sessions.compact", paramsJSON: json, timeoutSeconds: 10)
54182
}
55183

56184
func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload {
57-
struct Params: Codable { var sessionKey: String }
58-
let data = try JSONEncoder().encode(Params(sessionKey: sessionKey))
59-
let json = String(data: data, encoding: .utf8)
185+
let json = try Self.makeHistoryParamsJSON(sessionKey: sessionKey)
60186
let res = try await self.gateway.request(method: "chat.history", paramsJSON: json, timeoutSeconds: 15)
61187
return try JSONDecoder().decode(OpenClawChatHistoryPayload.self, from: res)
62188
}
@@ -73,35 +199,52 @@ struct IOSGatewayChatTransport: OpenClawChatTransport {
73199
+ "len=\(message.count) attachments=\(attachments.count)"
74200
Self.logger.info(
75201
"\(startLogMessage, privacy: .public)")
76-
struct Params: Codable {
77-
var sessionKey: String
78-
var message: String
79-
var thinking: String
80-
var attachments: [OpenClawChatAttachmentPayload]?
81-
var timeoutMs: Int
82-
var idempotencyKey: String
83-
}
84-
85-
let params = Params(
202+
GatewayDiagnostics.log(startLogMessage)
203+
let json = try Self.makeChatSendParamsJSON(
86204
sessionKey: sessionKey,
87205
message: message,
88206
thinking: thinking,
89-
attachments: attachments.isEmpty ? nil : attachments,
90-
timeoutMs: 30000,
91-
idempotencyKey: idempotencyKey)
92-
let data = try JSONEncoder().encode(params)
93-
let json = String(data: data, encoding: .utf8)
207+
idempotencyKey: idempotencyKey,
208+
attachments: attachments)
94209
do {
95210
let res = try await self.gateway.request(method: "chat.send", paramsJSON: json, timeoutSeconds: 35)
96211
let decoded = try JSONDecoder().decode(OpenClawChatSendResponse.self, from: res)
97212
Self.logger.info("chat.send ok runId=\(decoded.runId, privacy: .public)")
213+
GatewayDiagnostics.log("chat.send ok runId=\(decoded.runId) status=\(decoded.status)")
98214
return decoded
99215
} catch {
100216
Self.logger.error("chat.send failed \(error.localizedDescription, privacy: .public)")
217+
GatewayDiagnostics.log("chat.send failed error=\(error.localizedDescription)")
101218
throw error
102219
}
103220
}
104221

222+
func waitForRunCompletion(runId rawRunId: String, timeoutMs: Int) async -> Bool {
223+
let runId = rawRunId.trimmingCharacters(in: .whitespacesAndNewlines)
224+
guard !runId.isEmpty else { return false }
225+
226+
do {
227+
let json = try Self.makeAgentWaitParamsJSON(runId: runId, timeoutMs: timeoutMs)
228+
let requestTimeoutSeconds = Self.agentWaitRequestTimeoutSeconds(timeoutMs: timeoutMs)
229+
GatewayDiagnostics.log("agent.wait start runId=\(runId)")
230+
let res = try await self.gateway.request(
231+
method: "agent.wait",
232+
paramsJSON: json,
233+
timeoutSeconds: requestTimeoutSeconds)
234+
let completion = try Self.decodeAgentWaitCompletion(res, fallbackRunId: runId)
235+
GatewayDiagnostics.log("agent.wait completed runId=\(completion.runId) status=\(completion.status)")
236+
if !completion.completed {
237+
Self.logger.warning(
238+
"agent.wait status \(completion.status, privacy: .public) runId=\(runId, privacy: .public)")
239+
}
240+
return completion.completed
241+
} catch {
242+
Self.logger.warning("agent.wait failed \(error.localizedDescription, privacy: .public)")
243+
GatewayDiagnostics.log("agent.wait failed runId=\(runId) error=\(error.localizedDescription)")
244+
return false
245+
}
246+
}
247+
105248
func requestHealth(timeoutMs: Int) async throws -> Bool {
106249
let seconds = max(1, Int(ceil(Double(timeoutMs) / 1000.0)))
107250
let res = try await self.gateway.request(method: "health", paramsJSON: nil, timeoutSeconds: seconds)

0 commit comments

Comments
 (0)