mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
cb51cf7d3e
* feat(apps): adopt the system-notice family in Apple chat (OpenClawChatUI) Decode internal_system provenance and __openclaw history markers, classify once into message/notice/divider rows (web-parity kinds), render minimal hairline notice/divider SwiftUI rows, preserve metadata through history reconciliation, live transport, and the transcript cache, and align the transcript exporter with visible classification instead of leaking raw [System] prompts. * fix(apps): satisfy OpenClawKit periphery gate Remove the dead visibleMessages projection (rows path replaced its consumers) and annotate the provenance test-fixture initializer with the repo-standard periphery:ignore rationale. * fix(apps): refresh native i18n inventory after periphery cleanup
327 lines
10 KiB
Swift
327 lines
10 KiB
Swift
import Foundation
|
|
import OpenClawKit
|
|
import SwiftUI
|
|
|
|
private struct OpenClawChatPreviewTransport: OpenClawChatTransport {
|
|
enum Scenario {
|
|
case connected
|
|
case empty
|
|
case loading
|
|
case error
|
|
case systemNotices
|
|
}
|
|
|
|
let scenario: Scenario
|
|
|
|
init(scenario: Scenario = .connected) {
|
|
self.scenario = scenario
|
|
}
|
|
|
|
func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload {
|
|
switch self.scenario {
|
|
case .connected:
|
|
break
|
|
case .empty:
|
|
return OpenClawChatHistoryPayload(
|
|
sessionKey: sessionKey,
|
|
sessionId: "preview-empty-session",
|
|
messages: [],
|
|
thinkingLevel: "medium")
|
|
case .loading:
|
|
try await Task.sleep(nanoseconds: 60_000_000_000)
|
|
return OpenClawChatHistoryPayload(
|
|
sessionKey: sessionKey,
|
|
sessionId: "preview-loading-session",
|
|
messages: [],
|
|
thinkingLevel: "medium")
|
|
case .error:
|
|
throw NSError(
|
|
domain: "OpenClawChatPreviewTransport",
|
|
code: 1,
|
|
userInfo: [NSLocalizedDescriptionKey: "Gateway not connected. Check Tailscale and retry."])
|
|
case .systemNotices:
|
|
return OpenClawChatHistoryPayload(
|
|
sessionKey: sessionKey,
|
|
sessionId: "preview-system-notices",
|
|
messages: [
|
|
Self.systemNotice(
|
|
text: "[System] Resume the interrupted turn with internal recovery context.",
|
|
sourceTool: "main_session_restart_recovery",
|
|
timestamp: 1),
|
|
Self.systemNotice(
|
|
text: "[System] Gateway restarted after installing an update.",
|
|
sourceTool: "restart-sentinel",
|
|
timestamp: 2),
|
|
Self.historyMarker(
|
|
kind: "compaction",
|
|
id: "preview-compaction",
|
|
timestamp: 3,
|
|
tokensBefore: 48000,
|
|
tokensAfter: 19500),
|
|
Self.historyMarker(
|
|
kind: "reset",
|
|
id: "preview-reset",
|
|
timestamp: 4),
|
|
],
|
|
thinkingLevel: "medium")
|
|
}
|
|
|
|
return OpenClawChatHistoryPayload(
|
|
sessionKey: sessionKey,
|
|
sessionId: "preview-session",
|
|
messages: [
|
|
Self.message(
|
|
role: "user",
|
|
text: "Can you check the gateway status and summarize anything risky?",
|
|
timestamp: 1),
|
|
Self.message(
|
|
role: "assistant",
|
|
text: "Gateway is reachable. The only notable item is that push relay "
|
|
+ "is still using local distribution, so device tests should stay "
|
|
+ "on the local lane.",
|
|
timestamp: 2),
|
|
Self.toolCall(
|
|
id: "tool-preview-1",
|
|
name: "gateway.status",
|
|
arguments: ["deep": AnyCodable(true)],
|
|
timestamp: 3),
|
|
Self.toolResult(
|
|
toolCallId: "tool-preview-1",
|
|
name: "gateway.status",
|
|
text: "status=ok, channels=ios,macos, lastHeartbeat=12s",
|
|
timestamp: 4),
|
|
],
|
|
thinkingLevel: "medium")
|
|
}
|
|
|
|
func listModels() async throws -> [OpenClawChatModelChoice] {
|
|
[
|
|
OpenClawChatModelChoice(
|
|
modelID: "gpt-5.6-luna",
|
|
name: "GPT-5.6 Luna",
|
|
provider: "openai",
|
|
contextWindow: 400_000),
|
|
OpenClawChatModelChoice(
|
|
modelID: "sonnet-4.6",
|
|
name: "Claude Sonnet 4.6",
|
|
provider: "anthropic",
|
|
contextWindow: 200_000),
|
|
]
|
|
}
|
|
|
|
func sendMessage(
|
|
sessionKey _: String,
|
|
message _: String,
|
|
thinking _: String,
|
|
idempotencyKey: String,
|
|
attachments _: [OpenClawChatAttachmentPayload]) async throws -> OpenClawChatSendResponse
|
|
{
|
|
OpenClawChatSendResponse(runId: idempotencyKey, status: "ok")
|
|
}
|
|
|
|
func listSessions(
|
|
limit _: Int?,
|
|
search _: String?,
|
|
archived _: Bool) async throws -> OpenClawChatSessionsListResponse
|
|
{
|
|
OpenClawChatSessionsListResponse(
|
|
ts: 0,
|
|
path: nil,
|
|
count: 2,
|
|
defaults: OpenClawChatSessionsDefaults(
|
|
modelProvider: "openai",
|
|
model: "gpt-5.6-luna",
|
|
contextTokens: 400_000,
|
|
thinkingLevels: [
|
|
OpenClawChatThinkingLevelOption(id: "off", label: "off"),
|
|
OpenClawChatThinkingLevelOption(id: "medium", label: "medium"),
|
|
OpenClawChatThinkingLevelOption(id: "high", label: "high"),
|
|
],
|
|
thinkingDefault: "medium",
|
|
mainSessionKey: "main"),
|
|
sessions: [
|
|
Self.session(key: "main", displayName: "Main", updatedAt: 2),
|
|
Self.session(key: "ios-preview", displayName: "iOS preview", updatedAt: 1),
|
|
])
|
|
}
|
|
|
|
func requestHealth(timeoutMs _: Int) async throws -> Bool {
|
|
switch self.scenario {
|
|
case .connected, .empty, .loading, .systemNotices:
|
|
true
|
|
case .error:
|
|
false
|
|
}
|
|
}
|
|
|
|
func events() -> AsyncStream<OpenClawChatTransportEvent> {
|
|
AsyncStream { continuation in
|
|
continuation.finish()
|
|
}
|
|
}
|
|
|
|
func setActiveSessionKey(_: String) async throws {}
|
|
|
|
private static func message(role: String, text: String, timestamp: Double) -> AnyCodable {
|
|
AnyCodable([
|
|
"role": role,
|
|
"content": [["type": "text", "text": text]],
|
|
"timestamp": timestamp,
|
|
])
|
|
}
|
|
|
|
private static func systemNotice(text: String, sourceTool: String, timestamp: Double) -> AnyCodable {
|
|
AnyCodable([
|
|
"role": "user",
|
|
"content": [["type": "text", "text": text]],
|
|
"timestamp": timestamp,
|
|
"provenance": [
|
|
"kind": "internal_system",
|
|
"sourceTool": sourceTool,
|
|
],
|
|
])
|
|
}
|
|
|
|
private static func historyMarker(
|
|
kind: String,
|
|
id: String,
|
|
timestamp: Double,
|
|
tokensBefore: Double? = nil,
|
|
tokensAfter: Double? = nil) -> AnyCodable
|
|
{
|
|
var marker: [String: Any] = ["kind": kind, "id": id]
|
|
marker["tokensBefore"] = tokensBefore
|
|
marker["tokensAfter"] = tokensAfter
|
|
return AnyCodable([
|
|
"role": "system",
|
|
"content": [],
|
|
"timestamp": timestamp,
|
|
"__openclaw": marker,
|
|
])
|
|
}
|
|
|
|
private static func toolCall(
|
|
id: String,
|
|
name: String,
|
|
arguments: [String: AnyCodable],
|
|
timestamp: Double) -> AnyCodable
|
|
{
|
|
AnyCodable([
|
|
"role": "assistant",
|
|
"content": [
|
|
[
|
|
"type": "toolCall",
|
|
"id": id,
|
|
"name": name,
|
|
"arguments": AnyCodable(arguments),
|
|
],
|
|
],
|
|
"timestamp": timestamp,
|
|
])
|
|
}
|
|
|
|
private static func toolResult(
|
|
toolCallId: String,
|
|
name: String,
|
|
text: String,
|
|
timestamp: Double) -> AnyCodable
|
|
{
|
|
AnyCodable([
|
|
"role": "tool",
|
|
"content": [["type": "text", "text": text]],
|
|
"timestamp": timestamp,
|
|
"toolCallId": toolCallId,
|
|
"toolName": name,
|
|
])
|
|
}
|
|
|
|
private static func session(
|
|
key: String,
|
|
displayName: String,
|
|
updatedAt: Double) -> OpenClawChatSessionEntry
|
|
{
|
|
OpenClawChatSessionEntry(
|
|
key: key,
|
|
kind: nil,
|
|
displayName: displayName,
|
|
surface: "ios",
|
|
subject: nil,
|
|
room: nil,
|
|
space: nil,
|
|
updatedAt: updatedAt,
|
|
sessionId: nil,
|
|
systemSent: nil,
|
|
abortedLastRun: nil,
|
|
thinkingLevel: "medium",
|
|
verboseLevel: nil,
|
|
inputTokens: 2500,
|
|
outputTokens: 900,
|
|
totalTokens: 3400,
|
|
modelProvider: "openai",
|
|
model: "gpt-5.6-luna",
|
|
contextTokens: 400_000)
|
|
}
|
|
}
|
|
|
|
#if os(iOS)
|
|
#Preview("Chat") {
|
|
OpenClawChatPreview(scenario: .connected)
|
|
}
|
|
|
|
#Preview("Chat connected") {
|
|
OpenClawChatPreview(scenario: .connected)
|
|
}
|
|
|
|
#Preview("Chat empty") {
|
|
OpenClawChatPreview(
|
|
scenario: .empty,
|
|
sessionKey: "empty-preview")
|
|
}
|
|
|
|
#Preview("Chat loading") {
|
|
OpenClawChatPreview(
|
|
scenario: .loading,
|
|
sessionKey: "loading-preview")
|
|
}
|
|
|
|
#Preview("Chat gateway error") {
|
|
OpenClawChatPreview(
|
|
scenario: .error,
|
|
sessionKey: "error-preview")
|
|
}
|
|
|
|
#Preview("System notices") {
|
|
OpenClawChatPreview(
|
|
scenario: .systemNotices,
|
|
sessionKey: "system-notices-preview")
|
|
}
|
|
|
|
#Preview("Onboarding chat") {
|
|
OpenClawChatView(
|
|
viewModel: OpenClawChatViewModel(
|
|
sessionKey: "ios-preview",
|
|
transport: OpenClawChatPreviewTransport()),
|
|
showsSessionSwitcher: false,
|
|
style: .onboarding,
|
|
markdownVariant: .standard,
|
|
userAccent: OpenClawChatTheme.accent)
|
|
}
|
|
#endif
|
|
|
|
private struct OpenClawChatPreview: View {
|
|
let scenario: OpenClawChatPreviewTransport.Scenario
|
|
var sessionKey: String = "main"
|
|
|
|
var body: some View {
|
|
OpenClawChatView(
|
|
viewModel: OpenClawChatViewModel(
|
|
sessionKey: self.sessionKey,
|
|
transport: OpenClawChatPreviewTransport(scenario: self.scenario)),
|
|
showsSessionSwitcher: true,
|
|
style: .standard,
|
|
markdownVariant: .standard,
|
|
userAccent: OpenClawChatTheme.accent,
|
|
showsAssistantTrace: true)
|
|
}
|
|
}
|