import Foundation import OpenClawChatUI import OpenClawProtocol enum AppleReviewDemoMode { static let setupCode = "APPLE-REVIEW-DEMO" static let gatewayName = "Apple Review Demo Gateway" static let gatewayAddress = "Local demo mode" static let gatewayID = "apple-review-demo" static func isSetupCode(_ value: String) -> Bool { value.trimmingCharacters(in: .whitespacesAndNewlines) .localizedCaseInsensitiveCompare(self.setupCode) == .orderedSame } static var agents: [AgentSummary] { LocalChatFixture.appleReviewDemo.agents } } enum ScreenshotFixtureMode { static let gatewayName = "OpenClaw Gateway" static let gatewayAddress = "Gateway on local network" static let gatewayID = "screenshot-fixture-gateway" static var agents: [AgentSummary] { LocalChatFixture.appScreenshots.agents } } struct LocalChatFixture { let sessionKey: String let sessionIDPrefix: String let displayName: String let subject: String let modelProvider: String let modelID: String let modelName: String let responsePrefix: String let seedMessages: [String] let agents: [AgentSummary] static let appleReviewDemo = LocalChatFixture( sessionKey: "main", sessionIDPrefix: "apple-review-demo", displayName: "Apple Review Demo", subject: "Gateway review flow", modelProvider: "demo", modelID: "local-demo", modelName: "Apple Review Demo", responsePrefix: "Demo mode is active.", seedMessages: [ """ Apple Review demo mode is active. This local chat transport lets reviewers inspect the iOS app \ without a private Gateway. """, ], agents: [ AgentSummary( id: "main", name: "Main", identity: ["emoji": AnyCodable("OC")], workspace: "Apple Review Demo", workspacegit: false, model: ["provider": AnyCodable("demo"), "model": AnyCodable("local-demo")], agentruntime: ["kind": AnyCodable("local")], thinkinglevels: nil, thinkingoptions: ["auto", "low", "medium"], thinkingdefault: "auto"), ]) static let appScreenshots = LocalChatFixture( sessionKey: "main", sessionIDPrefix: "screenshot-fixture", displayName: "Molty", subject: "Mobile command center", modelProvider: "openai", modelID: "gpt-5.6-sol", modelName: "GPT-5.6 Sol", responsePrefix: "OpenClaw is connected to your gateway.", seedMessages: ProcessInfo.processInfo.arguments.contains("--openclaw-empty-chat-fixture") ? [] : ["Ready when you are. I can check a project, coordinate an agent, or prepare the next step."], agents: [ AgentSummary( id: "main", name: "Molty", identity: ["emoji": AnyCodable("M")], workspace: "OpenClaw", workspacegit: false, model: ["provider": AnyCodable("openai"), "model": AnyCodable("gpt-5.6-sol")], agentruntime: ["kind": AnyCodable("gateway")], thinkinglevels: nil, thinkingoptions: ["auto", "low", "medium", "high"], thinkingdefault: "auto"), AgentSummary( id: "research", name: "Research", identity: ["emoji": AnyCodable("RS")], workspace: "OpenClaw", workspacegit: false, model: ["provider": AnyCodable("openai"), "model": AnyCodable("gpt-5.6-sol")], agentruntime: ["kind": AnyCodable("gateway")], thinkinglevels: nil, thinkingoptions: ["auto", "low", "medium", "high"], thinkingdefault: "medium"), AgentSummary( id: "automation", name: "Automation", identity: ["emoji": AnyCodable("AU")], workspace: "OpenClaw", workspacegit: false, model: ["provider": AnyCodable("openai"), "model": AnyCodable("gpt-5.6-sol")], agentruntime: ["kind": AnyCodable("gateway")], thinkinglevels: nil, thinkingoptions: ["auto", "low", "medium", "high"], thinkingdefault: "auto"), ]) } struct LocalFixtureChatTransport: OpenClawChatTransport { private let fixture: LocalChatFixture private let store: LocalFixtureChatStore init(fixture: LocalChatFixture) { self.fixture = fixture self.store = LocalFixtureChatStore(fixture: fixture) } func createSession( key: String, label _: String?, parentSessionKey _: String?, worktree _: Bool?) async throws -> OpenClawChatCreateSessionResponse { try await self.store.createSession(key: key) } func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload { try await self.store.history(sessionKey: sessionKey) } func listModels(agentID _: String?) async throws -> [OpenClawChatModelChoice] { [ OpenClawChatModelChoice( modelID: self.fixture.modelID, name: self.fixture.modelName, provider: self.fixture.modelProvider, contextWindow: 128_000), ] } func isSwarmEnabled(sessionKey _: String) async throws -> Bool { ProcessInfo.processInfo.arguments.contains("--openclaw-swarm-chat-fixture") } func sendMessage( sessionKey: String, message: String, thinking _: String, idempotencyKey: String, attachments _: [OpenClawChatAttachmentPayload]) async throws -> OpenClawChatSendResponse { try await self.store.sendMessage( sessionKey: sessionKey, message: message, runId: idempotencyKey) } func abortRun(sessionKey: String, runId: String) async throws { await self.store.abortRun(sessionKey: sessionKey, runId: runId) } func listSessions( limit _: Int?, search: String?, archived: Bool) async throws -> OpenClawChatSessionsListResponse { let response = try await store.sessions() var sessions = response.sessions if archived { sessions = [] } if let search { sessions = OpenClawChatSessionListOrganizer.filter(sessions, search: search) } return OpenClawChatSessionsListResponse( ts: response.ts, path: response.path, count: sessions.count, defaults: response.defaults, sessions: sessions) } func listChildSessions(parentKey: String) async throws -> [OpenClawChatSessionEntry] { guard ProcessInfo.processInfo.arguments.contains("--openclaw-swarm-chat-fixture") else { return [] } let groupID = "swarm:\(parentKey):research" return [ self.swarmChild("polling", "National polling", status: "done", groupID: groupID, parentKey: parentKey), self.swarmChild("work", "Work and labor", status: "running", groupID: groupID, parentKey: parentKey), self.swarmChild("health", "Health", status: "running", groupID: groupID, parentKey: parentKey), self.swarmChild( "trust", "Governance and trust", status: nil, groupID: groupID, parentKey: parentKey, queued: true), self.swarmChild("media", "Media signals", status: "failed", groupID: groupID, parentKey: parentKey), ] } private func swarmChild( _ key: String, _ label: String, status: String?, groupID: String, parentKey: String, queued: Bool = false) -> OpenClawChatSessionEntry { OpenClawChatSessionEntry( key: "agent:main:subagent:\(key)", kind: "direct", displayName: label, surface: nil, subject: nil, room: nil, space: nil, updatedAt: 1, sessionId: nil, systemSent: nil, abortedLastRun: nil, thinkingLevel: nil, verboseLevel: nil, inputTokens: nil, outputTokens: nil, totalTokens: nil, modelProvider: self.fixture.modelProvider, model: self.fixture.modelID, contextTokens: 128_000, parentSessionKey: parentKey, spawnedBy: parentKey, status: status, hasActiveRun: status == "running", subagentRunState: queued ? "active" : nil, swarmGroupId: groupID, swarmPhase: "Research", swarmPhaseRank: 0, swarmLog: "Comparing labor, education, health, trust, and media signals.") } func setSessionModel(sessionKey _: String, model _: String?) async throws {} func setSessionThinking(sessionKey _: String, thinkingLevel _: String) async throws {} func requestHealth(timeoutMs _: Int) async throws -> Bool { true } /// The held screenshot run resolves only when the real composer aborts it. func waitForRunCompletion(runId: String, timeoutMs _: Int) async -> OpenClawChatRunObservation { await self.store.runObservation(runId: runId) } func events() -> AsyncStream { AsyncStream { continuation in continuation.yield(.health(ok: true)) self.registerFixtureEventContinuation(continuation) } } func setActiveSessionKey(_: String) async throws {} func resetSession(sessionKey _: String) async throws { await self.store.reset() } func compactSession(sessionKey _: String) async throws {} } struct AppleReviewDemoChatTransport: OpenClawChatTransport { private let transport = LocalFixtureChatTransport(fixture: .appleReviewDemo) func createSession( key: String, label: String?, parentSessionKey: String?, worktree: Bool?) async throws -> OpenClawChatCreateSessionResponse { try await self.transport.createSession( key: key, label: label, parentSessionKey: parentSessionKey, worktree: worktree) } func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload { try await self.transport.requestHistory(sessionKey: sessionKey) } func listModels(agentID: String?) async throws -> [OpenClawChatModelChoice] { try await self.transport.listModels(agentID: agentID) } func sendMessage( sessionKey: String, message: String, thinking: String, idempotencyKey: String, attachments: [OpenClawChatAttachmentPayload]) async throws -> OpenClawChatSendResponse { try await self.transport.sendMessage( sessionKey: sessionKey, message: message, thinking: thinking, idempotencyKey: idempotencyKey, attachments: attachments) } func abortRun(sessionKey: String, runId: String) async throws { try await self.transport.abortRun(sessionKey: sessionKey, runId: runId) } func listSessions( limit: Int?, search: String?, archived: Bool) async throws -> OpenClawChatSessionsListResponse { try await self.transport.listSessions(limit: limit, search: search, archived: archived) } func setSessionModel(sessionKey: String, model: String?) async throws { try await self.transport.setSessionModel(sessionKey: sessionKey, model: model) } func patchSessionModel( sessionKey: String, agentID: String?, model: String?) async throws -> OpenClawChatModelPatchResult? { try await self.transport.patchSessionModel( sessionKey: sessionKey, agentID: agentID, model: model) } func setSessionThinking(sessionKey: String, thinkingLevel: String) async throws { try await self.transport.setSessionThinking(sessionKey: sessionKey, thinkingLevel: thinkingLevel) } func requestHealth(timeoutMs: Int) async throws -> Bool { try await self.transport.requestHealth(timeoutMs: timeoutMs) } func waitForRunCompletion( runId: String, timeoutMs: Int) async -> OpenClawChatRunObservation { await self.transport.waitForRunCompletion(runId: runId, timeoutMs: timeoutMs) } func events() -> AsyncStream { self.transport.events() } func setActiveSessionKey(_ sessionKey: String) async throws { try await self.transport.setActiveSessionKey(sessionKey) } func resetSession(sessionKey: String) async throws { try await self.transport.resetSession(sessionKey: sessionKey) } func compactSession(sessionKey: String) async throws { try await self.transport.compactSession(sessionKey: sessionKey) } } private actor LocalFixtureChatStore { private let fixture: LocalChatFixture private var messages: [OpenClawChatMessage] init(fixture: LocalChatFixture) { self.fixture = fixture self.messages = Self.seedMessages(fixture: fixture) } func createSession(key: String) throws -> OpenClawChatCreateSessionResponse { try Self.decode( CreateSessionPayload(ok: true, key: key, sessionId: "\(self.fixture.sessionIDPrefix)-\(key)"), as: OpenClawChatCreateSessionResponse.self) } func history(sessionKey: String) throws -> OpenClawChatHistoryPayload { let normalizedSessionKey = Self.normalizedSessionKey(sessionKey, fallback: self.fixture.sessionKey) return try Self.decode( HistoryPayload( sessionKey: normalizedSessionKey, sessionId: "\(self.fixture.sessionIDPrefix)-\(normalizedSessionKey)", messages: self.messages, thinkingLevel: "auto"), as: OpenClawChatHistoryPayload.self) } func sendMessage(sessionKey _: String, message: String, runId: String) throws -> OpenClawChatSendResponse { let now = Date().timeIntervalSince1970 * 1000 self.messages.append( Self.message( role: "user", text: message, timestamp: now, idempotencyKey: "\(runId):user")) let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines) let subject = trimmed.isEmpty ? "that request" : "\"\(trimmed)\"" if ScreenshotFixtureMode.holdsInitialChatRun, self.fixture.sessionIDPrefix == "screenshot-fixture", !self.heldInitialRun { self.heldInitialRun = true self.activeRunID = runId return try Self.decode( SendPayload(runId: runId, status: "started"), as: OpenClawChatSendResponse.self) } self.messages.append( Self.message( role: "assistant", text: """ \(self.fixture.responsePrefix) I can help with \(subject), summarize current project context, \ prepare agent actions, and keep the mobile workflow connected to the gateway. """, timestamp: now + 1)) return try Self.decode( SendPayload(runId: runId, status: "ok"), as: OpenClawChatSendResponse.self) } private var heldInitialRun = false private var activeRunID: String? private var eventContinuation: AsyncStream.Continuation? func setEventContinuation(_ continuation: AsyncStream.Continuation) { self.eventContinuation = continuation } func runObservation(runId: String) -> OpenClawChatRunObservation { self.activeRunID == runId ? .checkAgain : .terminal(.completed) } func abortRun(sessionKey: String, runId: String) { guard self.activeRunID == runId else { return } self.activeRunID = nil self.eventContinuation?.yield(.chat(OpenClawChatEventPayload( runId: runId, sessionKey: sessionKey, state: "aborted", message: nil, errorMessage: nil))) } func sessions() throws -> OpenClawChatSessionsListResponse { let entry = OpenClawChatSessionEntry( key: fixture.sessionKey, kind: "chat", displayName: self.fixture.displayName, surface: "ios", subject: self.fixture.subject, room: nil, space: nil, updatedAt: Date().timeIntervalSince1970 * 1000, sessionId: "\(self.fixture.sessionIDPrefix)-\(self.fixture.sessionKey)", systemSent: true, abortedLastRun: false, thinkingLevel: "auto", verboseLevel: nil, inputTokens: nil, outputTokens: nil, totalTokens: nil, modelProvider: self.fixture.modelProvider, model: self.fixture.modelID, contextTokens: 128_000, thinkingLevels: Self.thinkingLevels, thinkingOptions: Self.thinkingOptions, thinkingDefault: "auto") return OpenClawChatSessionsListResponse( ts: Date().timeIntervalSince1970 * 1000, path: nil, count: 1, defaults: OpenClawChatSessionsDefaults( modelProvider: self.fixture.modelProvider, model: self.fixture.modelID, contextTokens: 128_000, thinkingLevels: Self.thinkingLevels, thinkingOptions: Self.thinkingOptions, thinkingDefault: "auto", mainSessionKey: self.fixture.sessionKey), sessions: [entry]) } func reset() { self.messages = Self.seedMessages(fixture: self.fixture) } private static var thinkingOptions: [String] { ["auto", "low", "medium", "high"] } private static var thinkingLevels: [OpenClawChatThinkingLevelOption] { [ OpenClawChatThinkingLevelOption(id: "auto", label: "Auto"), OpenClawChatThinkingLevelOption(id: "low", label: "Low"), OpenClawChatThinkingLevelOption(id: "medium", label: "Medium"), OpenClawChatThinkingLevelOption(id: "high", label: "High"), ] } private static func seedMessages(fixture: LocalChatFixture) -> [OpenClawChatMessage] { let now = Date().timeIntervalSince1970 * 1000 return fixture.seedMessages.enumerated().map { index, text in self.message(role: "assistant", text: text, timestamp: now + Double(index)) } } private static func message( role: String, text: String, timestamp: Double, idempotencyKey: String? = nil) -> OpenClawChatMessage { OpenClawChatMessage( role: role, content: [ OpenClawChatMessageContent( type: "text", text: text, mimeType: nil, fileName: nil, content: nil), ], timestamp: timestamp, idempotencyKey: idempotencyKey, stopReason: role == "assistant" ? "stop" : nil) } private static func normalizedSessionKey(_ value: String, fallback: String) -> String { let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) return trimmed.isEmpty ? fallback : trimmed } private static func decode(_ value: some Encodable, as type: T.Type) throws -> T { let data = try JSONEncoder().encode(value) return try JSONDecoder().decode(type, from: data) } private struct HistoryPayload: Encodable { var sessionKey: String var sessionId: String? var messages: [OpenClawChatMessage]? var thinkingLevel: String? } private struct SendPayload: Encodable { var runId: String var status: String } private struct CreateSessionPayload: Encodable { var ok: Bool? var key: String var sessionId: String? } } extension ScreenshotFixtureMode { static var holdsInitialChatRun: Bool { ProcessInfo.processInfo.arguments.contains("--openclaw-hold-initial-chat-run") } } extension LocalFixtureChatTransport { private func registerFixtureEventContinuation( _ continuation: AsyncStream.Continuation) { guard ScreenshotFixtureMode.holdsInitialChatRun else { continuation.finish() return } Task { await self.store.setEventContinuation(continuation) } } }