fix(chat): improve history + polish SwiftUI panel

This commit is contained in:
Peter Steinberger
2025-12-14 04:18:21 +00:00
parent 01341d983c
commit e0545e2f94
5 changed files with 61 additions and 44 deletions

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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() }
}

View File

@@ -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 },
);

View File

@@ -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 ??