fix: stabilize session previews
This commit is contained in:
@@ -145,10 +145,11 @@ extension MenuSessionsInjector {
|
|||||||
let headerItem = NSMenuItem()
|
let headerItem = NSMenuItem()
|
||||||
headerItem.tag = self.tag
|
headerItem.tag = self.tag
|
||||||
headerItem.isEnabled = false
|
headerItem.isEnabled = false
|
||||||
|
let statusText = self.cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState))
|
||||||
let hosted = self.makeHostedView(
|
let hosted = self.makeHostedView(
|
||||||
rootView: AnyView(MenuSessionsHeaderView(
|
rootView: AnyView(MenuSessionsHeaderView(
|
||||||
count: rows.count,
|
count: rows.count,
|
||||||
statusText: isConnected ? nil : self.controlChannelStatusText(for: channelState))),
|
statusText: statusText)),
|
||||||
width: width,
|
width: width,
|
||||||
highlighted: false)
|
highlighted: false)
|
||||||
headerItem.view = hosted
|
headerItem.view = hosted
|
||||||
@@ -598,8 +599,11 @@ extension MenuSessionsInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard self.isControlChannelConnected else {
|
guard self.isControlChannelConnected else {
|
||||||
self.cachedSnapshot = nil
|
if self.cachedSnapshot != nil {
|
||||||
self.cachedErrorText = nil
|
self.cachedErrorText = "Gateway disconnected (showing cached)"
|
||||||
|
} else {
|
||||||
|
self.cachedErrorText = nil
|
||||||
|
}
|
||||||
self.cacheUpdatedAt = Date()
|
self.cacheUpdatedAt = Date()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -624,8 +628,6 @@ extension MenuSessionsInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard self.isControlChannelConnected else {
|
guard self.isControlChannelConnected else {
|
||||||
self.cachedUsageSummary = nil
|
|
||||||
self.cachedUsageErrorText = nil
|
|
||||||
self.usageCacheUpdatedAt = Date()
|
self.usageCacheUpdatedAt = Date()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -648,8 +650,6 @@ extension MenuSessionsInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard self.isControlChannelConnected else {
|
guard self.isControlChannelConnected else {
|
||||||
self.cachedCostSummary = nil
|
|
||||||
self.cachedCostErrorText = nil
|
|
||||||
self.costCacheUpdatedAt = Date()
|
self.costCacheUpdatedAt = Date()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,51 @@ actor SessionPreviewCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actor SessionPreviewLimiter {
|
||||||
|
static let shared = SessionPreviewLimiter(maxConcurrent: 2)
|
||||||
|
|
||||||
|
private let maxConcurrent: Int
|
||||||
|
private var available: Int
|
||||||
|
private var waitQueue: [UUID] = []
|
||||||
|
private var waiters: [UUID: CheckedContinuation<Void, Never>] = [:]
|
||||||
|
|
||||||
|
init(maxConcurrent: Int) {
|
||||||
|
let normalized = max(1, maxConcurrent)
|
||||||
|
self.maxConcurrent = normalized
|
||||||
|
self.available = normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
func withPermit<T>(_ operation: () async throws -> T) async throws -> T {
|
||||||
|
await self.acquire()
|
||||||
|
defer { self.release() }
|
||||||
|
if Task.isCancelled { throw CancellationError() }
|
||||||
|
return try await operation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func acquire() async {
|
||||||
|
if self.available > 0 {
|
||||||
|
self.available -= 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let id = UUID()
|
||||||
|
await withCheckedContinuation { cont in
|
||||||
|
self.waitQueue.append(id)
|
||||||
|
self.waiters[id] = cont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func release() {
|
||||||
|
if let id = self.waitQueue.first {
|
||||||
|
self.waitQueue.removeFirst()
|
||||||
|
if let cont = self.waiters.removeValue(forKey: id) {
|
||||||
|
cont.resume()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.available = min(self.available + 1, self.maxConcurrent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
extension SessionPreviewCache {
|
extension SessionPreviewCache {
|
||||||
func _testSet(items: [SessionPreviewItem], for sessionKey: String, updatedAt: Date = Date()) {
|
func _testSet(items: [SessionPreviewItem], for sessionKey: String, updatedAt: Date = Date()) {
|
||||||
@@ -184,17 +229,31 @@ enum SessionMenuPreviewLoader {
|
|||||||
return self.snapshot(from: cached)
|
return self.snapshot(from: cached)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isConnected = await MainActor.run {
|
||||||
|
if case .connected = ControlChannel.shared.state { return true }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
guard isConnected else {
|
||||||
|
if let fallback = await SessionPreviewCache.shared.lastItems(for: sessionKey) {
|
||||||
|
return Self.snapshot(from: fallback)
|
||||||
|
}
|
||||||
|
return SessionMenuPreviewSnapshot(items: [], status: .error("Gateway disconnected"))
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let timeoutMs = Int(self.previewTimeoutSeconds * 1000)
|
let timeoutMs = Int(self.previewTimeoutSeconds * 1000)
|
||||||
let payload = try await AsyncTimeout.withTimeout(
|
let payload = try await SessionPreviewLimiter.shared.withPermit {
|
||||||
seconds: self.previewTimeoutSeconds,
|
try await AsyncTimeout.withTimeout(
|
||||||
onTimeout: { PreviewTimeoutError() },
|
seconds: self.previewTimeoutSeconds,
|
||||||
operation: {
|
onTimeout: { PreviewTimeoutError() },
|
||||||
try await GatewayConnection.shared.chatHistory(
|
operation: {
|
||||||
sessionKey: sessionKey,
|
try await GatewayConnection.shared.chatHistory(
|
||||||
limit: self.previewLimit(for: maxItems),
|
sessionKey: sessionKey,
|
||||||
timeoutMs: timeoutMs)
|
limit: self.previewLimit(for: maxItems),
|
||||||
})
|
timeoutMs: timeoutMs)
|
||||||
|
})
|
||||||
|
}
|
||||||
let built = Self.previewItems(from: payload, maxItems: maxItems)
|
let built = Self.previewItems(from: payload, maxItems: maxItems)
|
||||||
await SessionPreviewCache.shared.store(items: built, for: sessionKey)
|
await SessionPreviewCache.shared.store(items: built, for: sessionKey)
|
||||||
return Self.snapshot(from: built)
|
return Self.snapshot(from: built)
|
||||||
|
|||||||
Reference in New Issue
Block a user