From e0545e2f942a9b75b3f0802477b27b5d341a30ca Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 14 Dec 2025 04:18:21 +0000 Subject: [PATCH] fix(chat): improve history + polish SwiftUI panel --- .../Sources/Clawdis/GatewayChannel.swift | 5 +- .../Sources/Clawdis/WebChatSwiftUI.swift | 79 +++++++++++-------- .../Sources/ClawdisChatUI/ChatView.swift | 10 --- src/gateway/protocol/schema.ts | 1 + src/gateway/server.ts | 10 ++- 5 files changed, 61 insertions(+), 44 deletions(-) diff --git a/apps/macos/Sources/Clawdis/GatewayChannel.swift b/apps/macos/Sources/Clawdis/GatewayChannel.swift index 6f417e288..4fe68858d 100644 --- a/apps/macos/Sources/Clawdis/GatewayChannel.swift +++ b/apps/macos/Sources/Clawdis/GatewayChannel.swift @@ -43,7 +43,10 @@ protocol WebSocketSessioning: AnyObject { extension URLSession: WebSocketSessioning { func makeWebSocketTask(url: URL) -> WebSocketTaskBox { - WebSocketTaskBox(task: self.webSocketTask(with: url)) + let task = self.webSocketTask(with: url) + // Avoid "Message too long" receive errors for large snapshots / history payloads. + task.maximumMessageSize = 16 * 1024 * 1024 // 16 MB + return WebSocketTaskBox(task: task) } } diff --git a/apps/macos/Sources/Clawdis/WebChatSwiftUI.swift b/apps/macos/Sources/Clawdis/WebChatSwiftUI.swift index 540d71879..3a6c415da 100644 --- a/apps/macos/Sources/Clawdis/WebChatSwiftUI.swift +++ b/apps/macos/Sources/Clawdis/WebChatSwiftUI.swift @@ -72,35 +72,8 @@ struct MacGatewayChatTransport: ClawdisChatTransport, Sendable { let stream = await GatewayConnection.shared.subscribe() for await push in stream { if Task.isCancelled { return } - switch push { - case let .snapshot(hello): - let ok = (try? JSONDecoder().decode( - ClawdisGatewayHealthOK.self, - from: JSONEncoder().encode(hello.snapshot.health)))?.ok ?? true - continuation.yield(.health(ok: ok)) - case let .event(evt): - switch evt.event { - case "health": - guard let payload = evt.payload else { break } - let ok = (try? JSONDecoder().decode( - ClawdisGatewayHealthOK.self, - from: JSONEncoder().encode(payload)))?.ok ?? true - continuation.yield(.health(ok: ok)) - case "tick": - continuation.yield(.tick) - case "chat": - guard let payload = evt.payload else { break } - if let chat = try? JSONDecoder().decode( - ClawdisChatEventPayload.self, - from: JSONEncoder().encode(payload)) - { - continuation.yield(.chat(chat)) - } - default: - break - } - case .seqGap: - continuation.yield(.seqGap) + if let evt = Self.mapPushToTransportEvent(push) { + continuation.yield(evt) } } } @@ -110,6 +83,42 @@ struct MacGatewayChatTransport: ClawdisChatTransport, Sendable { } } } + + static func mapPushToTransportEvent(_ push: GatewayPush) -> ClawdisChatTransportEvent? { + switch push { + case let .snapshot(hello): + let ok = (try? JSONDecoder().decode( + ClawdisGatewayHealthOK.self, + from: JSONEncoder().encode(hello.snapshot.health)))?.ok ?? true + return .health(ok: ok) + + case let .event(evt): + switch evt.event { + case "health": + guard let payload = evt.payload else { return nil } + let ok = (try? JSONDecoder().decode( + ClawdisGatewayHealthOK.self, + from: JSONEncoder().encode(payload)))?.ok ?? true + return .health(ok: ok) + case "tick": + return .tick + case "chat": + guard let payload = evt.payload else { return nil } + guard let chat = try? JSONDecoder().decode( + ClawdisChatEventPayload.self, + from: JSONEncoder().encode(payload)) + else { + return nil + } + return .chat(chat) + default: + return nil + } + + case .seqGap: + return .seqGap + } + } } // MARK: - Window controller @@ -124,11 +133,19 @@ final class WebChatSwiftUIWindowController { var onClosed: (() -> Void)? var onVisibilityChanged: ((Bool) -> Void)? - init(sessionKey: String, presentation: WebChatPresentation) { + convenience init(sessionKey: String, presentation: WebChatPresentation) { + self.init(sessionKey: sessionKey, presentation: presentation, transport: MacGatewayChatTransport()) + } + + init(sessionKey: String, presentation: WebChatPresentation, transport: any ClawdisChatTransport) { self.sessionKey = sessionKey self.presentation = presentation - let vm = ClawdisChatViewModel(sessionKey: sessionKey, transport: MacGatewayChatTransport()) + let vm = ClawdisChatViewModel(sessionKey: sessionKey, transport: transport) self.hosting = NSHostingController(rootView: ClawdisChatView(viewModel: vm)) + self.hosting.view.wantsLayer = true + self.hosting.view.layer?.cornerCurve = .continuous + self.hosting.view.layer?.cornerRadius = 16 + self.hosting.view.layer?.masksToBounds = true self.window = Self.makeWindow(for: presentation, contentViewController: self.hosting) } diff --git a/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift b/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift index 7817d60be..8b07f009b 100644 --- a/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift +++ b/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift @@ -23,16 +23,6 @@ public struct ClawdisChatView: View { .padding(.vertical, 16) .frame(maxWidth: 1040) } - .background( - LinearGradient( - colors: [ - Color(red: 0.96, green: 0.97, blue: 1.0), - Color(red: 0.93, green: 0.94, blue: 0.98), - ], - startPoint: .top, - endPoint: .bottom) - .opacity(0.35) - .ignoresSafeArea()) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .onAppear { self.viewModel.load() } } diff --git a/src/gateway/protocol/schema.ts b/src/gateway/protocol/schema.ts index 0d4816d69..9f3c52aeb 100644 --- a/src/gateway/protocol/schema.ts +++ b/src/gateway/protocol/schema.ts @@ -458,6 +458,7 @@ export const CronRunLogEntrySchema = Type.Object( export const ChatHistoryParamsSchema = Type.Object( { sessionKey: NonEmptyString, + limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 500 })), }, { additionalProperties: false }, ); diff --git a/src/gateway/server.ts b/src/gateway/server.ts index 808cdf9c6..6d138f0c0 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -793,13 +793,19 @@ export async function startGatewayServer( }, }; } - const { sessionKey } = params as { sessionKey: string }; + const { sessionKey, limit } = params as { + sessionKey: string; + limit?: number; + }; const { storePath, entry } = loadSessionEntry(sessionKey); const sessionId = entry?.sessionId; - const messages = + const rawMessages = sessionId && storePath ? readSessionMessages(sessionId, storePath) : []; + const max = typeof limit === "number" ? limit : 200; + const messages = + rawMessages.length > max ? rawMessages.slice(-max) : rawMessages; const thinkingLevel = entry?.thinkingLevel ?? loadConfig().inbound?.reply?.thinkingDefault ??