mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-17 03:56:45 +00:00
Merge pull request #274 from getagentseal/fix/menubar-wake-tabs
Fix menubar stuck loading, double-click tabs, oversized disconnected tabs
This commit is contained in:
commit
63d4da609a
3 changed files with 32 additions and 36 deletions
|
|
@ -118,6 +118,11 @@ final class AppStore {
|
|||
|
||||
private var inFlightKeys: Set<PayloadCacheKey> = []
|
||||
|
||||
func resetLoadingState() {
|
||||
loadingCount = 0
|
||||
inFlightKeys.removeAll()
|
||||
}
|
||||
|
||||
private func invalidateStaleDayCache() {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|||
private var backgroundActivity: NSObjectProtocol?
|
||||
private var pendingRefreshWork: DispatchWorkItem?
|
||||
private var refreshLoopTask: Task<Void, Never>?
|
||||
private var forceRefreshTask: Task<Void, Never>?
|
||||
|
||||
func applicationWillFinishLaunching(_ notification: Notification) {
|
||||
// Set accessory policy before the app's focus chain forms. On macOS Tahoe
|
||||
|
|
@ -87,6 +88,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|||
queue: .main
|
||||
) { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
self?.forceRefreshTask?.cancel()
|
||||
self?.forceRefreshTask = nil
|
||||
self?.refreshLoopTask?.cancel()
|
||||
self?.refreshLoopTask = nil
|
||||
}
|
||||
|
|
@ -102,6 +105,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|||
queue: .main
|
||||
) { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
self?.store.resetLoadingState()
|
||||
self?.forceRefresh()
|
||||
if self?.refreshLoopTask == nil { self?.startRefreshLoop() }
|
||||
}
|
||||
|
|
@ -209,17 +213,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|||
guard now.timeIntervalSince(lastRefreshTime) > 5 else { return }
|
||||
lastRefreshTime = now
|
||||
|
||||
// Note: do NOT call store.invalidateCache() here. The day-rollover guard
|
||||
// inside refresh() already wipes the cache when the calendar date has
|
||||
// changed; wiping unconditionally on every wake/manual-refresh empties
|
||||
// todayPayload, makes showAgentTabs go false, and triggers the
|
||||
// full-popover loading overlay (because cache[key] == nil after wipe).
|
||||
// That's the regression chain documented in the multi-agent review.
|
||||
//
|
||||
// showLoading: true is fine now that the overlay condition is
|
||||
// `!hasCachedData`: it lights up the refresh-button spinner glyph
|
||||
// without covering the popover body.
|
||||
Task {
|
||||
forceRefreshTask?.cancel()
|
||||
forceRefreshTask = Task {
|
||||
async let main: Void = store.refresh(includeOptimize: false, force: true, showLoading: true)
|
||||
async let today: Void = store.refreshQuietly(period: .today)
|
||||
_ = await (main, today)
|
||||
|
|
|
|||
|
|
@ -7,17 +7,14 @@ struct AgentTabStrip: View {
|
|||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 5) {
|
||||
ForEach(visibleFilters) { filter in
|
||||
Button {
|
||||
AgentTab(
|
||||
filter: filter,
|
||||
cost: cost(for: filter),
|
||||
isActive: store.selectedProvider == filter,
|
||||
quota: store.quotaSummary(for: filter)
|
||||
) {
|
||||
store.switchTo(provider: filter)
|
||||
} label: {
|
||||
AgentTab(
|
||||
filter: filter,
|
||||
cost: cost(for: filter),
|
||||
isActive: store.selectedProvider == filter,
|
||||
quota: store.quotaSummary(for: filter)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
|
|
@ -65,10 +62,12 @@ private struct AgentTab: View {
|
|||
let cost: Double?
|
||||
let isActive: Bool
|
||||
let quota: QuotaSummary?
|
||||
let onTap: () -> Void
|
||||
|
||||
@State private var hoverPopoverShown = false
|
||||
@State private var hoverEnterTask: DispatchWorkItem?
|
||||
@State private var hoverExitTask: DispatchWorkItem?
|
||||
@State private var clickDismissed = false
|
||||
|
||||
/// Providers whose AgentTab chip reserves a 3pt bar slot underneath the
|
||||
/// label, even when not yet connected. Driven by which providers we
|
||||
|
|
@ -93,16 +92,9 @@ private struct AgentTab: View {
|
|||
.tracking(-0.2)
|
||||
}
|
||||
}
|
||||
// Reserve the bar slot only for providers whose quota source we
|
||||
// implement (Claude, Codex). Providers that will never have a bar
|
||||
// (All / Cursor / Droid / Gemini / Copilot) skip the slot entirely
|
||||
// so the text centers naturally and the chip stays compact.
|
||||
// Reserving the slot for Claude/Codex prevents the strip from
|
||||
// jumping by 6pt the moment the user clicks Connect.
|
||||
if Self.providerSupportsQuota(filter) {
|
||||
if quota != nil {
|
||||
AgentTabQuotaBar(quota: quota, isActive: isActive)
|
||||
.frame(height: 3)
|
||||
.opacity(quota == nil ? 0 : 1)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
|
|
@ -113,20 +105,24 @@ private struct AgentTab: View {
|
|||
)
|
||||
.foregroundStyle(isActive ? AnyShapeStyle(.white) : AnyShapeStyle(.secondary))
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
hoverPopoverShown = false
|
||||
hoverEnterTask?.cancel()
|
||||
clickDismissed = true
|
||||
onTap()
|
||||
}
|
||||
.onHover { hovering in
|
||||
// Debounce: 250ms enter so swiping across chips doesn't pop a
|
||||
// popover for every chip touched, and 150ms exit so cursor travel
|
||||
// between chip and popover doesn't dismiss prematurely.
|
||||
hoverEnterTask?.cancel()
|
||||
hoverExitTask?.cancel()
|
||||
if hovering, quota != nil {
|
||||
let task = DispatchWorkItem { hoverPopoverShown = true }
|
||||
hoverEnterTask = task
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: task)
|
||||
} else {
|
||||
if !hovering {
|
||||
clickDismissed = false
|
||||
let task = DispatchWorkItem { hoverPopoverShown = false }
|
||||
hoverExitTask = task
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15, execute: task)
|
||||
} else if !clickDismissed, quota != nil {
|
||||
let task = DispatchWorkItem { hoverPopoverShown = true }
|
||||
hoverEnterTask = task
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: task)
|
||||
}
|
||||
}
|
||||
.popover(isPresented: $hoverPopoverShown) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue