





















@@ -176,99 +176,31 @@ extension MenuSessionsInjector {
176176let channelState = ControlChannel.shared.state
177177178178var cursor = insertIndex
179-var headerView: NSView?
180-181-if let snapshot = self.cachedSnapshot {
182-let now = Date()
183-let mainKey = self.mainSessionKey
184-let rows = snapshot.rows.filter { row in
185-if row.key == "main", mainKey != "main" { return false }
186-if row.key == mainKey { return true }
187-guard let updatedAt = row.updatedAt else { return false }
188-return now.timeIntervalSince(updatedAt) <= self.activeWindowSeconds
189-}.sorted { lhs, rhs in
190-if lhs.key == mainKey { return true }
191-if rhs.key == mainKey { return false }
192-return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast)
193-}
194-if !rows.isEmpty {
195-let previewKeys = rows.prefix(20).map(\.key)
196-let task = Task {
197-await SessionMenuPreviewLoader.prewarm(sessionKeys: previewKeys, maxItems: 10)
198-}
199-self.previewTasks.append(task)
200-}
201-202-let headerItem = NSMenuItem()
203- headerItem.tag = self.tag
204- headerItem.isEnabled = false
205-let statusText = self
206-.cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState))
207-let hosted = self.makeHostedView(
208- rootView: AnyView(MenuSessionsHeaderView(
209- count: rows.count,
210- statusText: statusText)),
211- width: width,
212- highlighted: false)
213- headerItem.view = hosted
214- headerView = hosted
215- menu.insertItem(headerItem, at: cursor)
216- cursor += 1
217-218-if rows.isEmpty {
219- menu.insertItem(
220-self.makeMessageItem(text: "No active sessions", symbolName: "minus", width: width),
221- at: cursor)
222- cursor += 1
223-} else {
224-for row in rows {
225-let item = NSMenuItem()
226- item.tag = self.tag
227- item.isEnabled = true
228- item.submenu = self.buildSubmenu(for: row, storePath: snapshot.storePath)
229- item.view = self.makeHostedView(
230- rootView: AnyView(SessionMenuLabelView(row: row, width: width)),
231- width: width,
232- highlighted: true)
233- menu.insertItem(item, at: cursor)
234- cursor += 1
235-}
236-}
237-} else {
238-let headerItem = NSMenuItem()
239- headerItem.tag = self.tag
240- headerItem.isEnabled = false
241-let statusText = isConnected
242-? (self.cachedErrorText ?? "Loading sessions…")
243-: self.controlChannelStatusText(for: channelState)
244-let hosted = self.makeHostedView(
245- rootView: AnyView(MenuSessionsHeaderView(
246- count: 0,
247- statusText: statusText)),
248- width: width,
249- highlighted: false)
250- headerItem.view = hosted
251- headerView = hosted
252- menu.insertItem(headerItem, at: cursor)
253- cursor += 1
254-255-if !isConnected {
256- menu.insertItem(
257-self.makeMessageItem(
258- text: "Connect the gateway to see sessions",
259- symbolName: "bolt.slash",
260- width: width),
261- at: cursor)
262- cursor += 1
263-}
264-}
179+let item = NSMenuItem(title: "Context", action: nil, keyEquivalent: "")
180+ item.tag = self.tag
181+ item.isEnabled = true
182+ item.submenu = self.buildContextSubmenu(
183+ width: width,
184+ isConnected: isConnected,
185+ channelState: channelState)
186+let hosted = self.makeHostedView(
187+ rootView: AnyView(ContextRootMenuLabelView(
188+ subtitle: self.contextRootSubtitle(
189+ isConnected: isConnected,
190+ channelState: channelState),
191+ width: width)),
192+ width: width,
193+ highlighted: true)
194+ item.view = hosted
195+ menu.insertItem(item, at: cursor)
196+ cursor += 1
265197266198 cursor = self.insertUsageSection(into: menu, at: cursor, width: width)
267199 cursor = self.insertCostUsageSection(into: menu, at: cursor, width: width)
268200269-DispatchQueue.main.async { [weak self, weak headerView] in
270-guard let self, let headerView else { return }
271-self.captureMenuWidthIfAvailable(from: headerView)
201+DispatchQueue.main.async { [weak self, weak hosted] in
202+guard let self, let hosted else { return }
203+self.captureMenuWidthIfAvailable(from: hosted)
272204}
273205}
274206@@ -346,6 +278,124 @@ extension MenuSessionsInjector {
346278 _ = cursor
347279}
348280281+private func buildContextSubmenu(
282+ width: CGFloat,
283+ isConnected: Bool,
284+ channelState: ControlChannel.ConnectionState) -> NSMenu
285+{
286+let menu = NSMenu()
287+let width = max(300, width)
288+var cursor = 0
289+290+if let snapshot = self.cachedSnapshot {
291+let rows = self.activeRows(from: snapshot)
292+if !rows.isEmpty {
293+let previewKeys = rows.prefix(20).map(\.key)
294+let task = Task {
295+await SessionMenuPreviewLoader.prewarm(sessionKeys: previewKeys, maxItems: 10)
296+}
297+self.previewTasks.append(task)
298+}
299+300+let headerItem = NSMenuItem()
301+ headerItem.tag = self.tag
302+ headerItem.isEnabled = false
303+let statusText = self.cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState))
304+ headerItem.view = self.makeHostedView(
305+ rootView: AnyView(MenuSessionsHeaderView(
306+ count: rows.count,
307+ statusText: statusText)),
308+ width: width,
309+ highlighted: false)
310+ menu.insertItem(headerItem, at: cursor)
311+ cursor += 1
312+313+if rows.isEmpty {
314+ menu.insertItem(
315+self.makeMessageItem(text: "No active sessions", symbolName: "minus", width: width),
316+ at: cursor)
317+ cursor += 1
318+} else {
319+for row in rows {
320+let item = NSMenuItem()
321+ item.tag = self.tag
322+ item.isEnabled = true
323+ item.representedObject = row.key
324+ item.submenu = self.buildSubmenu(for: row, storePath: snapshot.storePath)
325+ item.view = self.makeHostedView(
326+ rootView: AnyView(SessionMenuLabelView(row: row, width: width)),
327+ width: width,
328+ highlighted: true)
329+ menu.insertItem(item, at: cursor)
330+ cursor += 1
331+}
332+}
333+} else {
334+let headerItem = NSMenuItem()
335+ headerItem.tag = self.tag
336+ headerItem.isEnabled = false
337+let statusText = isConnected
338+? (self.cachedErrorText ?? "Loading sessions…")
339+: self.controlChannelStatusText(for: channelState)
340+ headerItem.view = self.makeHostedView(
341+ rootView: AnyView(MenuSessionsHeaderView(
342+ count: 0,
343+ statusText: statusText)),
344+ width: width,
345+ highlighted: false)
346+ menu.insertItem(headerItem, at: cursor)
347+ cursor += 1
348+349+if !isConnected {
350+ menu.insertItem(
351+self.makeMessageItem(
352+ text: "Connect the gateway to see sessions",
353+ symbolName: "bolt.slash",
354+ width: width),
355+ at: cursor)
356+ cursor += 1
357+}
358+}
359+360+ _ = cursor
361+return menu
362+}
363+364+private func contextRootSubtitle(
365+ isConnected: Bool,
366+ channelState: ControlChannel.ConnectionState) -> String
367+{
368+if let snapshot = self.cachedSnapshot {
369+return self.sessionsSubtitle(count: self.activeRows(from: snapshot).count)
370+}
371+372+if isConnected {
373+return self.cachedErrorText ?? "Loading…"
374+}
375+376+return self.controlChannelStatusText(for: channelState)
377+}
378+379+private func activeRows(from snapshot: SessionStoreSnapshot) -> [SessionRow] {
380+let now = Date()
381+let mainKey = self.mainSessionKey
382+return snapshot.rows.filter { row in
383+if row.key == "main", mainKey != "main" { return false }
384+if row.key == mainKey { return true }
385+guard let updatedAt = row.updatedAt else { return false }
386+return now.timeIntervalSince(updatedAt) <= self.activeWindowSeconds
387+}.sorted { lhs, rhs in
388+if lhs.key == mainKey { return true }
389+if rhs.key == mainKey { return false }
390+return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast)
391+}
392+}
393+394+private func sessionsSubtitle(count: Int) -> String {
395+if count == 1 { return "1 session · 24h" }
396+return "\(count) sessions · 24h"
397+}
398+349399private func insertUsageSection(into menu: NSMenu, at cursor: Int, width: CGFloat) -> Int {
350400let rows = self.usageRows
351401if rows.isEmpty {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。