
























@@ -65,28 +65,28 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
6565self.inject(into: menu)
6666self.injectNodes(into: menu)
676768- // Refresh in background for the next open; keep width stable while open.
68+ // Refresh in the background for the next open. Rebuilding custom menu
69+ // rows while AppKit is tracking the menu causes visible flicker.
6970self.loadTask?.cancel()
70-let forceRefresh = self.cachedSnapshot == nil || self.cachedErrorText != nil
71-self.loadTask = Task { [weak self] in
71+let shouldRepaintAfterRefresh = self.cachedSnapshot == nil || self.cachedErrorText != nil
72+self.loadTask = Task { [weak self, weak menu] in
7273guard let self else { return }
74+let forceRefresh = shouldRepaintAfterRefresh
7375await self.refreshCache(force: forceRefresh)
7476await self.refreshUsageCache(force: forceRefresh)
7577await self.refreshCostUsageCache(force: forceRefresh)
76-await MainActor.run {
77-guard self.isMenuOpen else { return }
78-self.inject(into: menu)
79-self.injectNodes(into: menu)
78+if shouldRepaintAfterRefresh {
79+await self.repaintOpenMenu(menu)
8080}
8181}
82828383self.nodesLoadTask?.cancel()
84-self.nodesLoadTask = Task { [weak self] in
84+let shouldRepaintNodesAfterRefresh = self.shouldRepaintNodesAfterRefresh()
85+self.nodesLoadTask = Task { [weak self, weak menu] in
8586guard let self else { return }
8687await self.nodesStore.refresh()
87-await MainActor.run {
88-guard self.isMenuOpen else { return }
89-self.injectNodes(into: menu)
88+if !shouldRepaintAfterRefresh, shouldRepaintNodesAfterRefresh {
89+await self.repaintOpenMenuNodes(menu)
9090}
9191}
9292}
@@ -95,8 +95,6 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
9595self.originalDelegate?.menuDidClose?(menu)
9696self.isMenuOpen = false
9797self.menuOpenWidth = nil
98-self.loadTask?.cancel()
99-self.nodesLoadTask?.cancel()
10098self.cancelPreviewTasks()
10199}
102100@@ -122,25 +120,18 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
122120guard self.isMenuOpen, let menu = self.statusItem?.menu else { return }
123121self.loadTask?.cancel()
124122self.loadTask = Task { [weak self, weak menu] in
125-guard let self, let menu else { return }
123+guard let self else { return }
126124await self.refreshCache(force: true)
127125await self.refreshUsageCache(force: true)
128126await self.refreshCostUsageCache(force: true)
129-await MainActor.run {
130-guard self.isMenuOpen else { return }
131-self.inject(into: menu)
132-self.injectNodes(into: menu)
133-}
127+await self.repaintOpenMenu(menu)
134128}
135129136130self.nodesLoadTask?.cancel()
137131self.nodesLoadTask = Task { [weak self, weak menu] in
138-guard let self, let menu else { return }
132+guard let self else { return }
139133await self.nodesStore.refresh()
140-await MainActor.run {
141-guard self.isMenuOpen else { return }
142-self.injectNodes(into: menu)
143-}
134+await self.repaintOpenMenuNodes(menu)
144135}
145136}
146137@@ -278,6 +269,28 @@ extension MenuSessionsInjector {
278269 _ = cursor
279270}
280271272+private func repaintOpenMenu(_ menu: NSMenu?) async {
273+await MainActor.run {
274+guard self.isMenuOpen, let menu else { return }
275+self.inject(into: menu)
276+self.injectNodes(into: menu)
277+}
278+}
279+280+private func repaintOpenMenuNodes(_ menu: NSMenu?) async {
281+await MainActor.run {
282+guard self.isMenuOpen, let menu else { return }
283+self.injectNodes(into: menu)
284+}
285+}
286+287+private func shouldRepaintNodesAfterRefresh() -> Bool {
288+guard self.isControlChannelConnected else { return false }
289+return self.sortedNodeEntries().isEmpty
290+ || self.nodesStore.lastError?.nonEmpty != nil
291+ || self.nodesStore.statusMessage?.nonEmpty != nil
292+}
293+281294private func buildContextSubmenu(
282295 width: CGFloat,
283296 isConnected: Bool,
@@ -374,7 +387,7 @@ extension MenuSessionsInjector {
374387return self.cachedErrorText ?? "Loading…"
375388}
376389377-return self.controlChannelStatusText(for: channelState)
390+return Self.menuStatusText(self.controlChannelStatusText(for: channelState))
378391}
379392380393private func activeRows(from snapshot: SessionStoreSnapshot) -> [SessionRow] {
@@ -527,7 +540,7 @@ extension MenuSessionsInjector {
527540case .connecting:
528541"Connecting…"
529542case let .degraded(message):
530- message.nonEmpty ?? "Gateway disconnected"
543+Self.menuStatusText(message.nonEmpty ?? "Gateway disconnected")
531544case .disconnected:
532545"Gateway disconnected"
533546}
@@ -684,7 +697,7 @@ extension MenuSessionsInjector {
684697self.previewTasks.removeAll()
685698}
686699687-private func makeMessageItem(text: String, symbolName: String, width: CGFloat, maxLines: Int? = 2) -> NSMenuItem {
700+private func makeMessageItem(text: String, symbolName: String, width: CGFloat, maxLines: Int? = 3) -> NSMenuItem {
688701let view = AnyView(
689702HStack(alignment: .top, spacing: 8) {
690703Image(systemName: symbolName)
@@ -810,6 +823,19 @@ extension MenuSessionsInjector {
810823}
811824return "Sessions unavailable"
812825}
826+827+private static func menuStatusText(_ text: String) -> String {
828+let lines = text
829+.trimmingCharacters(in: .whitespacesAndNewlines)
830+.split(whereSeparator: \.isNewline)
831+.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
832+.filter { !$0.isEmpty }
833+834+let singleLine = (lines.isEmpty ? text : lines.joined(separator: " "))
835+.trimmingCharacters(in: .whitespacesAndNewlines)
836+guard singleLine.count > 180 else { return singleLine }
837+return "\(singleLine.prefix(177))…"
838+}
813839}
814840815841extension MenuSessionsInjector {
@@ -1283,6 +1309,14 @@ extension MenuSessionsInjector {
12831309self.inject(into: menu)
12841310}
128513111312+func testingControlChannelStatusText(for state: ControlChannel.ConnectionState) -> String {
1313+self.controlChannelStatusText(for: state)
1314+}
1315+1316+func testingMenuStatusText(_ text: String) -> String {
1317+Self.menuStatusText(text)
1318+}
1319+12861320func testingFindInsertIndex(in menu: NSMenu) -> Int? {
12871321self.findInsertIndex(in: menu)
12881322}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。